diff options
author | Ferdinand Thiessen <opensource@fthiessen.de> | 2024-03-13 00:37:02 +0100 |
---|---|---|
committer | Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.com> | 2024-03-14 20:45:24 +0100 |
commit | ac4003879d65f5cfaf27a8a4c90090fc62f3ce2d (patch) | |
tree | 8221b880f8536a9ce591a3a218f616998cf3101d /apps/settings/src | |
parent | aa29204fe0994190317eda1d6504fbb10ad61e29 (diff) | |
download | nextcloud-server-ac4003879d65f5cfaf27a8a4c90090fc62f3ce2d.tar.gz nextcloud-server-ac4003879d65f5cfaf27a8a4c90090fc62f3ce2d.zip |
feat(settings): Implement `carousel` type for app discover section
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
Diffstat (limited to 'apps/settings/src')
4 files changed, 222 insertions, 4 deletions
diff --git a/apps/settings/src/components/AppStoreDiscover/AppStoreDiscoverSection.vue b/apps/settings/src/components/AppStoreDiscover/AppStoreDiscoverSection.vue index 1118f23419f..68610347420 100644 --- a/apps/settings/src/components/AppStoreDiscover/AppStoreDiscoverSection.vue +++ b/apps/settings/src/components/AppStoreDiscover/AppStoreDiscoverSection.vue @@ -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 index 00000000000..d0f410b433d --- /dev/null +++ b/apps/settings/src/components/AppStoreDiscover/CarouselType.vue @@ -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> diff --git a/apps/settings/src/components/AppStoreDiscover/PostType.vue b/apps/settings/src/components/AppStoreDiscover/PostType.vue index 8b182b56566..9d19a1b4da4 100644 --- a/apps/settings/src/components/AppStoreDiscover/PostType.vue +++ b/apps/settings/src/components/AppStoreDiscover/PostType.vue @@ -20,14 +20,15 @@ - --> <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; diff --git a/apps/settings/src/constants/AppDiscoverTypes.ts b/apps/settings/src/constants/AppDiscoverTypes.ts index 606288f5967..d28516fe79c 100644 --- a/apps/settings/src/constants/AppDiscoverTypes.ts +++ b/apps/settings/src/constants/AppDiscoverTypes.ts @@ -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 |