Skip to content

Commit

Permalink
Merge pull request #4983 from nextcloud-libraries/enh/skip-buttons
Browse files Browse the repository at this point in the history
enh(NcContent): Add skip content buttons
  • Loading branch information
susnux authored Jan 20, 2024
2 parents c1908ee + dc5ef51 commit 808bbb2
Show file tree
Hide file tree
Showing 8 changed files with 255 additions and 10 deletions.
9 changes: 9 additions & 0 deletions l10n/messages.pot
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,9 @@ msgstr ""
msgid "invisible"
msgstr ""

msgid "Keyboard navigation help"
msgstr ""

msgid "Load more \"{options}\""
msgstr ""

Expand Down Expand Up @@ -313,6 +316,12 @@ msgstr ""
msgid "Show password"
msgstr ""

msgid "Skip to app navigation"
msgstr ""

msgid "Skip to main content"
msgstr ""

msgid "Smart Picker"
msgstr ""

Expand Down
12 changes: 12 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
],
"dependencies": {
"@floating-ui/dom": "^1.1.0",
"@linusborg/vue-simple-portal": "^0.1.5",
"@nextcloud/auth": "^2.0.0",
"@nextcloud/axios": "^2.0.0",
"@nextcloud/browser-storage": "^0.3.0",
Expand Down
24 changes: 21 additions & 3 deletions src/components/NcAppNavigation/NcAppNavigation.vue
Original file line number Diff line number Diff line change
Expand Up @@ -78,21 +78,29 @@ emit('toggle-navigation', {
</template>

<script>
import NcAppNavigationToggle from '../NcAppNavigationToggle/index.js'
import { useIsMobile } from '../../composables/useIsMobile/index.js'
import { getTrapStack } from '../../utils/focusTrap.js'

import { emit, subscribe, unsubscribe } from '@nextcloud/event-bus'

import { createFocusTrap } from 'focus-trap'

import NcAppNavigationToggle from '../NcAppNavigationToggle/index.js'
import Vue from 'vue'

export default {
name: 'NcAppNavigation',

components: {
NcAppNavigationToggle,
},

// Injected from NcContent
inject: {
setHasAppNavigation: {
default: () => () => Vue.util.warn('NcAppNavigation is not mounted inside NcContent, this is probably an error.'),
from: 'NcContent:setHasAppNavigation',
},
},

props: {
/**
* The aria label to describe the navigation
Expand Down Expand Up @@ -135,6 +143,7 @@ export default {
},

mounted() {
this.setHasAppNavigation(true)
subscribe('toggle-navigation', this.toggleNavigationByEventBus)
// Emit an event with the initial state of the navigation
emit('navigation-toggled', {
Expand All @@ -150,6 +159,7 @@ export default {
this.toggleFocusTrap()
},
unmounted() {
this.setHasAppNavigation(false)
unsubscribe('toggle-navigation', this.toggleNavigationByEventBus)
this.focusTrap.deactivate()
},
Expand All @@ -161,6 +171,14 @@ export default {
* @param {boolean} [state] set the state instead of inverting the current one
*/
toggleNavigation(state) {
// Early return if alreay in that state
if (this.open === state) {
emit('navigation-toggled', {
open: this.open,
})
return
}

this.open = (typeof state === 'undefined') ? !this.open : state
const bodyStyles = getComputedStyle(document.body)
const animationLength = parseInt(bodyStyles.getPropertyValue('--animation-quick')) || 100
Expand Down
149 changes: 146 additions & 3 deletions src/components/NcContent/NcContent.vue
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ This component provides the default container of all apps.
It _MUST_ be used as the main wrapper of your app.
It includes the Navigation, the App content and the Sidebar.

It also will set the skip content buttons needed for accessibility.

### Standard usage

```vue
Expand Down Expand Up @@ -69,25 +71,166 @@ It includes the Navigation, the App content and the Sidebar.
</docs>

<template>
<div id="content-vue"
:class="`app-${appName.toLowerCase()}`"
class="content">
<div id="content-vue" :class="['content', `app-${appName.toLowerCase()}`]">
<!-- TODO: with vue3 the `selector` attribute needs to be changed to `to="#skip-actions"` -->
<Teleport selector="#skip-actions">
<div class="vue-skip-actions__container">
<div class="vue-skip-actions__headline">
{{ t('Keyboard navigation help') }}
</div>
<div class="vue-skip-actions__buttons">
<NcButton v-show="hasAppNavigation"
type="tertiary"
href="#app-navigation-vue"
@click.prevent="openAppNavigation"
@focusin="currentFocus = 'navigation'"
@mouseover="currentFocus = 'navigation'">
{{ t('Skip to app navigation') }}
</NcButton>
<NcButton type="tertiary"
href="#app-content-vue"
@focusin="currentFocus = 'content'"
@mouseover="currentFocus = 'content'">
{{ t('Skip to main content') }}
</NcButton>
</div>
<NcIconSvgWrapper v-show="!isMobile"
class="vue-skip-actions__image"
:svg="currentImage"
size="auto" />
</div>
&nbsp;<!-- TODO: Remove with vue3! This is a bug of vue-simple-portal that does not allow a single child, ref LinusBorg/vue-simple-portal#20 -->
</Teleport>
<slot />
</div>
</template>

<script>
import { emit } from '@nextcloud/event-bus'
// TODO: This is built-in for vue3 just drop the import
import { Portal as Teleport } from '@linusborg/vue-simple-portal'
import { useIsMobile } from '../../composables/useIsMobile/index.js'
import { t } from '../../l10n.js'

import NcButton from '../NcButton/NcButton.vue'
import NcIconSvgWrapper from '../NcIconSvgWrapper/NcIconSvgWrapper.vue'

/* eslint-disable import/no-unresolved */
import contentSvg from './content-selected.svg?raw'
import navigationSvg from './navigation-selected.svg?raw'
/* eslint-enable import/no-unresolved */

export default {
name: 'NcContent',
components: {
NcButton,
NcIconSvgWrapper,
Teleport,
},
provide() {
return {
'NcContent:setHasAppNavigation': this.setAppNavigation,
}
},
props: {
appName: {
type: String,
required: true,
},
},
setup() {
const isMobile = useIsMobile()
return {
isMobile,
}
},
data() {
return {
hasAppNavigation: false,
currentFocus: '', // unknown
}
},
computed: {
currentImage() {
if (this.currentFocus === 'navigation') {
return navigationSvg
}
return contentSvg
},
},
beforeMount() {
const container = document.getElementById('skip-actions')
if (container) {
// clear default buttons
container.innerHTML = ''
// add class for scoping styles
container.classList.add('vue-skip-actions')
}
},
methods: {
t,
openAppNavigation() {
emit('toggle-navigation', { open: true })
this.$nextTick(() => {
window.location.hash = 'app-navigation-vue'
// we need to manually focus if the window location is already set to the app-navigation then it will not focus again
document.getElementById('app-navigation-vue').focus()
})
},
setAppNavigation(value) {
this.hasAppNavigation = value
// If app navigation is available and no focus was set yet, set it to navigation as it is the first button
if (this.currentFocus === '') {
this.currentFocus = 'navigation'
}
},
},
}
</script>

<style lang="scss">
// Remove server stylings and add a backdrop
#skip-actions.vue-skip-actions:focus-within {
top: 0!important;
left: 0!important;
width: 100vw;
height: 100vh;
padding: var(--body-container-margin)!important;
backdrop-filter: brightness(50%);
}
</style>

<style lang="scss" scoped>
.vue-skip-actions {
&__container {
background-color: var(--color-main-background);
border-radius: var(--border-radius-large);
padding: 22px;
}

&__headline {
font-weight: bold;
font-size: 20px;
line-height: 30px;
margin-bottom: 12px;
}

&__buttons {
display: flex;
flex-wrap: wrap;
gap: 12px;

> * {
// Ensure buttons are same width on smaller screens (container wrapped)
flex: 1 0 fit-content;
}
}

&__image {
margin-top: 12px;
}
}

.content {
box-sizing: border-box;
margin: var(--body-container-margin);
Expand Down
22 changes: 22 additions & 0 deletions src/components/NcContent/content-selected.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
22 changes: 22 additions & 0 deletions src/components/NcContent/navigation-selected.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
26 changes: 22 additions & 4 deletions src/components/NcIconSvgWrapper/NcIconSvgWrapper.vue
Original file line number Diff line number Diff line change
Expand Up @@ -131,9 +131,27 @@ export default {
type: String,
default: '',
},

/**
* Size of the icon to show. Only use if not using within an icon slot.
* Defaults to 20px which is the Nextcloud icon size for all icon slots.
* @default 20
*/
size: {
type: [Number, String],
default: 20,
validator: (value) => typeof value === 'number' || value === 'auto',
},
},

computed: {
/**
* Icon size used in CSS
*/
iconSize() {
return typeof this.size === 'number' ? `${this.size}px` : this.size
},

cleanSvg() {
if (!this.svg || this.path) {
return
Expand Down Expand Up @@ -177,10 +195,10 @@ export default {

&:deep(svg) {
fill: currentColor;
width: 20px;
height: 20px;
max-width: 20px;
max-height: 20px;
width: v-bind('iconSize');
height: v-bind('iconSize');
max-width: v-bind('iconSize');
max-height: v-bind('iconSize');
}
}
</style>

0 comments on commit 808bbb2

Please sign in to comment.