]> source.dussan.org Git - nextcloud-server.git/commitdiff
feat(settings): Implement `carousel` type for app discover section
authorFerdinand Thiessen <opensource@fthiessen.de>
Tue, 12 Mar 2024 23:37:02 +0000 (00:37 +0100)
committerBenjamin Gaussorgues <benjamin.gaussorgues@nextcloud.com>
Thu, 14 Mar 2024 19:45:24 +0000 (20:45 +0100)
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
apps/settings/src/components/AppStoreDiscover/AppStoreDiscoverSection.vue
apps/settings/src/components/AppStoreDiscover/CarouselType.vue [new file with mode: 0644]
apps/settings/src/components/AppStoreDiscover/PostType.vue
apps/settings/src/constants/AppDiscoverTypes.ts

index 1118f23419f6c2a963a0f54e5d7a7d4dae0ec108..686103474200dc870bab4496528c8722d782fe95 100644 (file)
@@ -41,6 +41,7 @@ import logger from '../../logger'
 import { apiTypeParser } from '../../utils/appDiscoverTypeParser.ts'
 
 const PostType = defineAsyncComponent(() => import('./PostType.vue'))
+const CarouselType = defineAsyncComponent(() => import('./CarouselType.vue'))
 
 const hasError = ref(false)
 const elements = ref<IAppDiscoverElements[]>([])
@@ -75,6 +76,8 @@ onBeforeMount(async () => {
 const getComponent = (type) => {
        if (type === 'post') {
                return PostType
+       } else if (type === 'carousel') {
+               return CarouselType
        }
        return defineComponent({
                mounted: () => logger.error('Unknown component requested ', type),
diff --git a/apps/settings/src/components/AppStoreDiscover/CarouselType.vue b/apps/settings/src/components/AppStoreDiscover/CarouselType.vue
new file mode 100644 (file)
index 0000000..d0f410b
--- /dev/null
@@ -0,0 +1,202 @@
+<template>
+       <section :aria-roledescription="t('settings', 'Carousel')" :aria-labelledby="headingId ? `${headingId}` : undefined">
+               <h3 v-if="headline" :id="headingId">
+                       {{ translatedHeadline }}
+               </h3>
+               <div class="app-discover-carousel__wrapper">
+                       <div class="app-discover-carousel__button-wrapper">
+                               <NcButton class="app-discover-carousel__button app-discover-carousel__button--previous"
+                                       type="tertiary-no-background"
+                                       :aria-label="t('settings', 'Previous slide')"
+                                       :disabled="!hasPrevious"
+                                       @click="currentIndex -= 1">
+                                       <template #icon>
+                                               <NcIconSvgWrapper :path="mdiChevronLeft" />
+                                       </template>
+                               </NcButton>
+                       </div>
+
+                       <Transition :name="transitionName" mode="out-in">
+                               <PostType v-bind="shownElement"
+                                       :key="shownElement.id ?? currentIndex"
+                                       :aria-labelledby="`${internalId}-tab-${currentIndex}`"
+                                       :dom-id="`${internalId}-tabpanel-${currentIndex}`"
+                                       inline
+                                       role="tabpanel" />
+                       </Transition>
+
+                       <div class="app-discover-carousel__button-wrapper">
+                               <NcButton class="app-discover-carousel__button app-discover-carousel__button--next"
+                                       type="tertiary-no-background"
+                                       :aria-label="t('settings', 'Next slide')"
+                                       :disabled="!hasNext"
+                                       @click="currentIndex += 1">
+                                       <template #icon>
+                                               <NcIconSvgWrapper :path="mdiChevronRight" />
+                                       </template>
+                               </NcButton>
+                       </div>
+               </div>
+               <div class="app-discover-carousel__tabs" role="tablist" :aria-label="t('settings', 'Choose slide to display')">
+                       <NcButton v-for="index of content.length"
+                               :id="`${internalId}-tab-${index}`"
+                               :key="index"
+                               :aria-label="t('settings', '{index} of {total}', { index, total: content.length })"
+                               :aria-controls="`${internalId}-tabpanel-${index}`"
+                               :aria-selected="`${currentIndex === (index - 1)}`"
+                               role="tab"
+                               type="tertiary-no-background"
+                               @click="currentIndex = index - 1">
+                               <template #icon>
+                                       <NcIconSvgWrapper :path="currentIndex === (index - 1) ? mdiCircleSlice8 : mdiCircleOutline" />
+                               </template>
+                       </NcButton>
+               </div>
+       </section>
+</template>
+
+<script lang="ts">
+import type { PropType } from 'vue'
+import type { IAppDiscoverCarousel } from '../../constants/AppDiscoverTypes.ts'
+
+import { mdiChevronLeft, mdiChevronRight, mdiCircleOutline, mdiCircleSlice8 } from '@mdi/js'
+import { translate as t } from '@nextcloud/l10n'
+import { computed, defineComponent, nextTick, ref, watch } from 'vue'
+import { commonAppDiscoverProps } from './common.ts'
+import { useLocalizedValue } from '../../composables/useGetLocalizedValue.ts'
+
+import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
+import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js'
+import PostType from './PostType.vue'
+
+export default defineComponent({
+       name: 'CarouselType',
+
+       components: {
+               NcButton,
+               NcIconSvgWrapper,
+               PostType,
+       },
+
+       props: {
+               ...commonAppDiscoverProps,
+
+               /**
+                * The content of the carousel
+                */
+               content: {
+                       type: Array as PropType<IAppDiscoverCarousel['content']>,
+                       required: true,
+               },
+       },
+
+       setup(props) {
+               const translatedHeadline = useLocalizedValue(computed(() => props.headline))
+
+               const currentIndex = ref(Math.min(1, props.content.length - 1))
+               const shownElement = ref(props.content[currentIndex.value])
+               const hasNext = computed(() => currentIndex.value < (props.content.length - 1))
+               const hasPrevious = computed(() => currentIndex.value > 0)
+
+               const internalId = computed(() => props.id ?? (Math.random() + 1).toString(36).substring(7))
+               const headingId = computed(() => `${internalId.value}-h`)
+
+               const transitionName = ref('slide-out')
+               watch(() => currentIndex.value, (o, n) => {
+                       if (o < n) {
+                               transitionName.value = 'slide-out'
+                       } else {
+                               transitionName.value = 'slide-in'
+                       }
+
+                       // Wait next tick
+                       nextTick(() => {
+                               shownElement.value = props.content[currentIndex.value]
+                       })
+               })
+
+               return {
+                       t,
+                       internalId,
+                       headingId,
+
+                       hasNext,
+                       hasPrevious,
+                       currentIndex,
+                       shownElement,
+
+                       transitionName,
+
+                       translatedHeadline,
+
+                       mdiChevronLeft,
+                       mdiChevronRight,
+                       mdiCircleOutline,
+                       mdiCircleSlice8,
+               }
+       },
+})
+</script>
+
+<style scoped lang="scss">
+h3 {
+       font-size: 24px;
+       font-weight: 600;
+       margin-block: 0 1em;
+}
+
+.app-discover-carousel {
+       &__wrapper {
+               display: flex;
+       }
+
+       &__button {
+               color: var(--color-text-maxcontrast);
+               position: absolute;
+               top: calc(50% - 22px); // 50% minus half of button height
+
+               &-wrapper {
+                       position: relative;
+               }
+
+               // See padding of discover section
+               &--next {
+                       right: -54px;
+               }
+               &--previous {
+                       left: -54px;
+               }
+       }
+
+       &__tabs {
+               display: flex;
+               flex-direction: row;
+               justify-content: center;
+
+               > * {
+                       color: var(--color-text-maxcontrast);
+               }
+       }
+}
+</style>
+
+<style>
+.slide-in-enter-active,
+.slide-in-leave-active,
+.slide-out-enter-active,
+.slide-out-leave-active {
+  transition: all .4s ease-out;
+}
+
+.slide-in-leave-to,
+.slide-out-enter {
+  opacity: 0;
+  transform: translateX(50%);
+}
+
+.slide-in-enter,
+.slide-out-leave-to {
+  opacity: 0;
+  transform: translateX(-50%);
+}
+</style>
index 8b182b56566d60bfb64ebe7843d58ef63fe75540..9d19a1b4da4b73d9833a020be40c6913730857c3 100644 (file)
   -
   -->
 <template>
-       <article class="app-discover-post"
+       <article :id="domId"
+               class="app-discover-post"
                :class="{ 'app-discover-post--reverse': media && media.alignment === 'start' }">
                <component :is="link ? 'a' : 'div'"
                        v-if="headline || text"
                        :href="link"
                        :target="link ? '_blank' : undefined"
                        class="app-discover-post__text">
-                       <h3>{{ translatedHeadline }}</h3>
+                       <component :is="inline ? 'h4' : 'h3'">{{ translatedHeadline }}</component>
                        <p>{{ translatedText }}</p>
                </component>
                <component :is="mediaLink ? 'a' : 'div'"
@@ -97,6 +98,18 @@ export default defineComponent({
                        required: false,
                        default: () => null,
                },
+
+               inline: {
+                       type: Boolean,
+                       required: false,
+                       default: false,
+               },
+
+               domId: {
+                       type: String,
+                       required: false,
+                       default: null,
+               },
        },
 
        setup(props) {
@@ -178,7 +191,7 @@ export default defineComponent({
                flex-direction: row-reverse;
        }
 
-       h3 {
+       h3, h4 {
                font-size: 24px;
                font-weight: 600;
                margin-block: 0 1em;
index 606288f596792d5522dd93f9535d470800da57cc..d28516fe79c60693dfeed3a9cc0fbc7933ee24b8 100644 (file)
@@ -123,7 +123,7 @@ export interface IAppDiscoverShowcase extends IAppDiscoverElement {
 export interface IAppDiscoverCarousel extends IAppDiscoverElement {
        type: 'carousel'
        text?: ILocalizedValue<string>
-       content: (IAppDiscoverPost | IAppDiscoverApp)[]
+       content: IAppDiscoverPost[]
 }
 
 export type IAppDiscoverElements = IAppDiscoverPost | IAppDiscoverCarousel | IAppDiscoverShowcase