aboutsummaryrefslogtreecommitdiffstats
path: root/core/src/components/AppMenu.vue
diff options
context:
space:
mode:
Diffstat (limited to 'core/src/components/AppMenu.vue')
-rw-r--r--core/src/components/AppMenu.vue275
1 files changed, 70 insertions, 205 deletions
diff --git a/core/src/components/AppMenu.vue b/core/src/components/AppMenu.vue
index e84a1250222..33e1a194f3c 100644
--- a/core/src/components/AppMenu.vue
+++ b/core/src/components/AppMenu.vue
@@ -6,99 +6,99 @@
<template>
<nav class="app-menu"
:aria-label="t('core', 'Applications menu')">
- <ul class="app-menu-main">
- <li v-for="app in mainAppList"
+ <ul class="app-menu__list">
+ <AppMenuEntry v-for="app in mainAppList"
:key="app.id"
- :data-app-id="app.id"
- class="app-menu-entry"
- :class="{ 'app-menu-entry__active': app.active }">
- <a :href="app.href"
- :class="{ 'has-unread': app.unread > 0 }"
- :aria-label="appLabel(app)"
- :title="app.name"
- :aria-current="app.active ? 'page' : false"
- :target="app.target ? '_blank' : undefined"
- :rel="app.target ? 'noopener noreferrer' : undefined">
- <img :src="app.icon" alt="">
- <div class="app-menu-entry--label">
- {{ app.name }}
- <span v-if="app.unread > 0" class="hidden-visually unread-counter">{{ app.unread }}</span>
- </div>
- </a>
- </li>
+ :app="app" />
</ul>
- <NcActions class="app-menu-more" :aria-label="t('core', 'More apps')">
+ <NcActions class="app-menu__overflow" :aria-label="t('core', 'More apps')">
<NcActionLink v-for="app in popoverAppList"
:key="app.id"
- :aria-label="appLabel(app)"
:aria-current="app.active ? 'page' : false"
:href="app.href"
- class="app-menu-popover-entry">
- <template #icon>
- <div class="app-icon" :class="{ 'has-unread': app.unread > 0 }">
- <img :src="app.icon" alt="">
- </div>
- </template>
- {{ app.name }}
- <span v-if="app.unread > 0" class="hidden-visually unread-counter">{{ app.unread }}</span>
- </NcActionLink>
+ :icon="app.icon"
+ :name="app.name"
+ class="app-menu__overflow-entry" />
</NcActions>
</nav>
</template>
-<script>
-import { loadState } from '@nextcloud/initial-state'
+<script lang="ts">
+import type { INavigationEntry } from '../types/navigation'
+
import { subscribe, unsubscribe } from '@nextcloud/event-bus'
+import { loadState } from '@nextcloud/initial-state'
+import { n, t } from '@nextcloud/l10n'
+import { defineComponent } from 'vue'
+
+import AppMenuEntry from './AppMenuEntry.vue'
import NcActions from '@nextcloud/vue/dist/Components/NcActions.js'
import NcActionLink from '@nextcloud/vue/dist/Components/NcActionLink.js'
+import logger from '../logger'
-export default {
+export default defineComponent({
name: 'AppMenu',
+
components: {
- NcActions, NcActionLink,
+ AppMenuEntry,
+ NcActions,
+ NcActionLink,
+ },
+
+ setup() {
+ return {
+ t,
+ n,
+ }
},
+
data() {
+ const appList = loadState<INavigationEntry[]>('core', 'apps', [])
+
return {
- apps: loadState('core', 'apps', {}),
+ appList,
appLimit: 0,
- observer: null,
+ observer: null as ResizeObserver | null,
}
},
+
computed: {
- appList() {
- return Object.values(this.apps)
- },
mainAppList() {
return this.appList.slice(0, this.appLimit)
},
popoverAppList() {
return this.appList.slice(this.appLimit)
},
- appLabel() {
- return (app) => app.name
- + (app.active ? ' (' + t('core', 'Currently open') + ')' : '')
- + (app.unread > 0 ? ' (' + n('core', '{count} notification', '{count} notifications', app.unread, { count: app.unread }) + ')' : '')
- },
},
+
mounted() {
this.observer = new ResizeObserver(this.resize)
this.observer.observe(this.$el)
this.resize()
subscribe('nextcloud:app-menu.refresh', this.setApps)
},
+
beforeDestroy() {
- this.observer.disconnect()
+ this.observer!.disconnect()
unsubscribe('nextcloud:app-menu.refresh', this.setApps)
},
+
methods: {
- setNavigationCounter(id, counter) {
- this.$set(this.apps[id], 'unread', counter)
+ setNavigationCounter(id: string, counter: number) {
+ const app = this.appList.find(({ app }) => app === id)
+ if (app) {
+ this.$set(app, 'unread', counter)
+ } else {
+ logger.warn(`Could not find app "${id}" for setting navigation count`)
+ }
},
- setApps({ apps }) {
- this.apps = apps
+
+ setApps({ apps }: { apps: INavigationEntry[]}) {
+ this.appList = apps
},
+
resize() {
- const availableWidth = this.$el.offsetWidth
+ const availableWidth = (this.$el as HTMLElement).offsetWidth
let appCount = Math.floor(availableWidth / 50) - 1
const popoverAppCount = this.appList.length - appCount
if (popoverAppCount === 1) {
@@ -110,183 +110,48 @@ export default {
this.appLimit = appCount
},
},
-}
+})
</script>
-<style lang="scss" scoped>
-$header-icon-size: 20px;
-
+<style scoped lang="scss">
.app-menu {
width: 100%;
display: flex;
flex-shrink: 1;
flex-wrap: wrap;
-}
-.app-menu-main {
- display: flex;
- flex-wrap: nowrap;
- .app-menu-entry {
- width: 50px;
- height: 50px;
- position: relative;
+ &__list {
display: flex;
+ flex-wrap: nowrap;
+ }
- &.app-menu-entry__active {
- opacity: 1;
-
- &::before {
- content: " ";
- position: absolute;
- pointer-events: none;
- border-bottom-color: var(--color-main-background);
- transform: translateX(-50%);
- width: 12px;
- height: 5px;
- border-radius: 3px;
- background-color: var(--color-background-plain-text);
- left: 50%;
- bottom: 6px;
- display: block;
- transition: all 0.1s ease-in-out;
- opacity: 1;
- }
-
- .app-menu-entry--label {
- font-weight: bold;
- }
- }
-
- a {
- width: calc(100% - 4px);
- height: calc(100% - 4px);
- margin: 2px;
- // this is shown directly on the background
- color: var(--color-background-plain-text);
- position: relative;
- }
-
- img {
- transition: margin 0.1s ease-in-out;
- width: $header-icon-size;
- height: $header-icon-size;
- padding: calc((100% - $header-icon-size) / 2);
- box-sizing: content-box;
- filter: var(--background-image-invert-if-bright);
- }
+ // Adjust the overflow NcActions styles as they are directly rendered on the background
+ &__overflow :deep(.button-vue--vue-tertiary) {
+ opacity: .7;
+ margin: 3px;
+ filter: var(--background-image-invert-if-bright);
- .app-menu-entry--label {
- opacity: 0;
- position: absolute;
- font-size: 12px;
- // this is shown directly on the background
+ /* Remove all background and align text color if not expanded */
+ &:not([aria-expanded="true"]) {
color: var(--color-background-plain-text);
- text-align: center;
- left: 50%;
- top: 45%;
- display: block;
- min-width: 100%;
- transform: translateX(-50%);
- transition: all 0.1s ease-in-out;
- width: 100%;
- text-overflow: ellipsis;
- overflow: hidden;
- letter-spacing: -0.5px;
- }
- &:hover,
- &:focus-within {
- opacity: 1;
- .app-menu-entry--label {
+ &:hover {
opacity: 1;
- font-weight: bolder;
- bottom: 0;
- width: 100%;
- text-overflow: ellipsis;
- overflow: hidden;
+ background-color: transparent !important;
}
}
- }
-
- // Show labels
- &:hover,
- &:focus-within,
- .app-menu-entry:hover,
- .app-menu-entry:focus {
- opacity: 1;
-
- img {
- margin-top: -8px;
- }
-
- .app-menu-entry--label {
- opacity: 1;
- bottom: 0;
- }
-
- &::before, .app-menu-entry::before {
- opacity: 0;
- }
- }
-}
-
-::v-deep .app-menu-more .button-vue--vue-tertiary {
- opacity: .7;
- margin: 3px;
- filter: var(--background-image-invert-if-bright);
-
- /* Remove all background and align text color if not expanded */
- &:not([aria-expanded="true"]) {
- color: var(--color-background-plain-text);
-
- &:hover {
+ &:focus-visible {
opacity: 1;
- background-color: transparent !important;
+ outline: none !important;
}
}
- &:focus-visible {
- opacity: 1;
- outline: none !important;
- }
-}
-
-.app-menu-popover-entry {
- .app-icon {
- position: relative;
- height: 44px;
- width: 48px;
- display: flex;
- align-items: center;
- justify-content: center;
- /* Icons are bright so invert them if bright color theme == bright background is used */
- filter: var(--background-invert-if-bright);
-
- &.has-unread::after {
- background-color: var(--color-main-text);
- }
-
- img {
- width: $header-icon-size;
- height: $header-icon-size;
+ &__overflow-entry {
+ :deep(.action-link__icon) {
+ // Icons are bright so invert them if bright color theme == bright background is used
+ filter: var(--background-invert-if-bright) !important;
}
}
}
-
-.has-unread::after {
- content: "";
- width: 8px;
- height: 8px;
- background-color: var(--color-background-plain-text);
- border-radius: 50%;
- position: absolute;
- display: block;
- top: 10px;
- right: 10px;
-}
-
-.unread-counter {
- display: none;
-}
</style>