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

enh(NcContent): Add skip content buttons #4983

Merged
merged 5 commits into from
Jan 20, 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
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.'),
Comment on lines +97 to +99
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we show a warning here, we should add something to the Files app to not show the warning

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would say it depends. Is there any good reason to not use NcContent?
If yes: Then we can remove the warning and adjust the docs.
If not: We should adjust files to use it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There isn't, this is the only way we ask Devs to build Vue apps

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
}
ShGKme marked this conversation as resolved.
Show resolved Hide resolved

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>
Loading