feat(settings): Implement `carousel` type for app discover section

Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
pull/44129/head
Ferdinand Thiessen 3 months ago committed by Benjamin Gaussorgues
parent aa29204fe0
commit ac4003879d
No known key found for this signature in database
GPG Key ID: 5DAC1CAFAA6DB883

@ -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),

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

@ -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;

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

Loading…
Cancel
Save