diff --git a/packages/pluggableWidgets/carousel-native/src/Carousel.editorConfig.ts b/packages/pluggableWidgets/carousel-native/src/Carousel.editorConfig.ts index d45a26aaf..ff0b5f602 100644 --- a/packages/pluggableWidgets/carousel-native/src/Carousel.editorConfig.ts +++ b/packages/pluggableWidgets/carousel-native/src/Carousel.editorConfig.ts @@ -1,4 +1,5 @@ import { RowLayoutProps, StructurePreviewProps, topBar } from "@mendix/piw-utils-internal"; +import { hidePropertiesIn, Properties } from "@mendix/pluggable-widgets-tools"; import paginationSVG from "./assets/pagination.svg"; @@ -37,3 +38,11 @@ export function getPreview(values: CarouselPreviewProps, isDarkMode: boolean): S return topBar("Carousel", content, isDarkMode); } + +export function getProperties(values: CarouselPreviewProps, defaultProperties: Properties): Properties { + if (!values.activeSelection) { + hidePropertiesIn(defaultProperties, values, ["onChangeAction", "animateExpression"]); + } + + return defaultProperties; +} diff --git a/packages/pluggableWidgets/carousel-native/src/Carousel.tsx b/packages/pluggableWidgets/carousel-native/src/Carousel.tsx index cf2e93c57..93119b2f2 100644 --- a/packages/pluggableWidgets/carousel-native/src/Carousel.tsx +++ b/packages/pluggableWidgets/carousel-native/src/Carousel.tsx @@ -23,17 +23,71 @@ export const Carousel = (props: CarouselProps): ReactElement => { const [activeSlide, setActiveSlide] = useState(0); + const [firstItem, setFirstItem] = useState(0); + const [loading, setLoading] = useState(true); useEffect(() => { - if (props.contentSource?.status === ValueStatus.Available) { + if (props.contentSource?.status === ValueStatus.Available && loading) { + // Set initial index of the first item to show the associated active selection. + const index = + (props.activeSelection?.value + ? props.contentSource?.items?.findIndex(i => i.id === props.activeSelection?.value?.id) + : 0) ?? 0; + setFirstItem(index); + setActiveSlide(index); setLoading(false); } - }, [props.contentSource]); + }, [loading, props.activeSelection, props.contentSource]); - const onSnap = useCallback((index: number) => { - setActiveSlide(index); - }, []); + useEffect(() => { + if ( + carouselRef && + props.contentSource.status === "available" && + props.activeSelection?.status === "available" + ) { + let index = props.contentSource.items?.findIndex(i => i.id === props.activeSelection?.value?.id) ?? 0; + // Removed item that is active selection can not be found + index = index >= 0 ? index : 0; + // Should check carouselRef.currentIndex though this is not fast enough for update. + if (index !== activeSlide) { + // Update carousel when associated item is changed + setActiveSlide(index); + const animate = props.animateExpression?.value ?? true; + // Async snap to index, use case add item is added before current selected + setTimeout(() => { + (carouselRef as NativeCarousel).snapToItem(index, animate); + }, 1); + } + } + }, [activeSlide, carouselRef, props.activeSelection, props.animateExpression, props.contentSource]); + + useEffect(() => { + if (props.contentSource.status === "available" && props.activeSelection?.status === "available") { + // Check if selected item is still available, reset to index 0 or null + let item = props.contentSource.items?.find(i => i.id === props.activeSelection?.value?.id); + if (item == null) { + item = props.contentSource.items?.[0]; + } + if (props.activeSelection.value?.id !== item?.id) { + // Set association when empty to first slide + props.activeSelection.setValue(item); + } + } + }, [props.activeSelection, props.contentSource]); + + const onSnap = useCallback( + (index: number) => { + setActiveSlide(index); + if (props.activeSelection) { + const item = props.contentSource?.items?.[index]; + if (item?.id !== props.activeSelection.value?.id) { + props.activeSelection.setValue(item); + } + } + }, + [props.activeSelection, props.contentSource] + ); const renderItem = useCallback(({ item, index }: { item: ObjectItem; index: number }) => { const viewStyle = layoutSpecificStyle.slideItem; @@ -97,7 +151,7 @@ export const Carousel = (props: CarouselProps): ReactElement => { ); }, [activeSlide, carouselRef, props.contentSource, props.showPagination]); - const onLayout = (event: LayoutChangeEvent) => { + const onLayout = (event: LayoutChangeEvent): void => { let viewHeight = event.nativeEvent.layout.height; const viewWidth = event.nativeEvent.layout.width; @@ -149,7 +203,7 @@ export const Carousel = (props: CarouselProps): ReactElement => { testID={`${props.name}$carousel`} activeSlideAlignment={props.activeSlideAlignment} layout="default" - firstItem={0} + firstItem={firstItem} useScrollView enableSnap data={props.contentSource.items} diff --git a/packages/pluggableWidgets/carousel-native/src/Carousel.xml b/packages/pluggableWidgets/carousel-native/src/Carousel.xml index 9a08efc0e..9a6a3f778 100644 --- a/packages/pluggableWidgets/carousel-native/src/Carousel.xml +++ b/packages/pluggableWidgets/carousel-native/src/Carousel.xml @@ -18,6 +18,20 @@ Content + + Active selection + + + + + + + + + Animate changed + Animate when 'Active selection' association is changed, animation on user swiping will always be on. + + @@ -41,6 +55,12 @@ + + + On change + When active selection association is changed. + + diff --git a/packages/pluggableWidgets/carousel-native/typings/CarouselProps.d.ts b/packages/pluggableWidgets/carousel-native/typings/CarouselProps.d.ts index 8868d9ce4..58959f676 100644 --- a/packages/pluggableWidgets/carousel-native/typings/CarouselProps.d.ts +++ b/packages/pluggableWidgets/carousel-native/typings/CarouselProps.d.ts @@ -4,7 +4,7 @@ * @author Mendix Widgets Framework Team */ import { ComponentType, CSSProperties, ReactNode } from "react"; -import { ListValue, ListWidgetValue } from "mendix"; +import { DynamicValue, ListValue, ListWidgetValue, ReferenceValue } from "mendix"; export type LayoutEnum = "card" | "fullWidth"; @@ -15,6 +15,8 @@ export interface CarouselProps