From 7e9a916e43bea364af27bc424f1e7b635f2a9bba Mon Sep 17 00:00:00 2001 From: Srirang Kalantri Date: Wed, 23 Sep 2026 16:25:15 +0530 Subject: [PATCH] fix: gallery native widget horizontal scroll --- .../gallery-native/CHANGELOG.md | 7 ++++ .../gallery-native/package.json | 2 +- .../src/Gallery.editorConfig.ts | 10 +++-- .../gallery-native/src/Gallery.tsx | 1 + .../gallery-native/src/Gallery.xml | 9 ++++ .../src/__mocks__/@shopify/flash-list.tsx | 4 +- .../gallery-native/src/components/Gallery.tsx | 42 +++++++++++++++---- .../src/components/__tests__/Gallery.spec.tsx | 34 +++++++++++++++ .../gallery-native/src/package.xml | 2 +- .../gallery-native/typings/GalleryProps.d.ts | 4 ++ 10 files changed, 100 insertions(+), 15 deletions(-) diff --git a/packages/pluggableWidgets/gallery-native/CHANGELOG.md b/packages/pluggableWidgets/gallery-native/CHANGELOG.md index d7b45c55e..c5054a463 100644 --- a/packages/pluggableWidgets/gallery-native/CHANGELOG.md +++ b/packages/pluggableWidgets/gallery-native/CHANGELOG.md @@ -7,6 +7,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased] - We've fixed an issue where Gallery widget does not display data. +### Fixed + +- We've fixed an issue where items in a horizontally scrolling Gallery would stretch to the full screen width instead of sizing to their content. + +### Added + +- Added horizontal item sizing options to fit items to their content or divide the Gallery width into configurable columns. ## [2.2.1] - 2026-6-10 diff --git a/packages/pluggableWidgets/gallery-native/package.json b/packages/pluggableWidgets/gallery-native/package.json index 6ab6f707d..282da9eb2 100644 --- a/packages/pluggableWidgets/gallery-native/package.json +++ b/packages/pluggableWidgets/gallery-native/package.json @@ -1,7 +1,7 @@ { "name": "gallery-native", "widgetName": "Gallery", - "version": "2.2.2", + "version": "2.3.0", "description": "A flexible gallery widget that renders columns, rows and layouts.", "copyright": "© Mendix Technology BV 2022. All rights reserved.", "license": "Apache-2.0", diff --git a/packages/pluggableWidgets/gallery-native/src/Gallery.editorConfig.ts b/packages/pluggableWidgets/gallery-native/src/Gallery.editorConfig.ts index c1d18cb72..4f7bfcc17 100644 --- a/packages/pluggableWidgets/gallery-native/src/Gallery.editorConfig.ts +++ b/packages/pluggableWidgets/gallery-native/src/Gallery.editorConfig.ts @@ -14,10 +14,14 @@ export function getProperties(values: GalleryPreviewProps, defaultProperties: Pr hidePropertyIn(defaultProperties, values, "loadMoreButtonCaption"); } - if (values.scrollDirection === "horizontal") { + if (values.scrollDirection === "vertical") { + hidePropertyIn(defaultProperties, values, "horizontalItemSizing"); + } else { hidePropertyIn(defaultProperties, values, "pullDown"); - hidePropertyIn(defaultProperties, values, "tabletColumns"); - hidePropertyIn(defaultProperties, values, "phoneColumns"); + if (values.horizontalItemSizing !== "fillColumns") { + hidePropertyIn(defaultProperties, values, "tabletColumns"); + hidePropertyIn(defaultProperties, values, "phoneColumns"); + } } if (values.filterList?.length === 0) { diff --git a/packages/pluggableWidgets/gallery-native/src/Gallery.tsx b/packages/pluggableWidgets/gallery-native/src/Gallery.tsx index 2cd0062da..4d6752328 100644 --- a/packages/pluggableWidgets/gallery-native/src/Gallery.tsx +++ b/packages/pluggableWidgets/gallery-native/src/Gallery.tsx @@ -120,6 +120,7 @@ export const Gallery = (props: GalleryProps): ReactElement => { pullDown={props.pullDown && pullDown} pullDownIsExecuting={props.pullDown?.isExecuting ?? false} scrollDirection={props.scrollDirection} + horizontalItemSizing={props.horizontalItemSizing} style={styles} tabletColumns={props.tabletColumns} /> diff --git a/packages/pluggableWidgets/gallery-native/src/Gallery.xml b/packages/pluggableWidgets/gallery-native/src/Gallery.xml index 2f149df77..c8e90c665 100644 --- a/packages/pluggableWidgets/gallery-native/src/Gallery.xml +++ b/packages/pluggableWidgets/gallery-native/src/Gallery.xml @@ -24,6 +24,15 @@ Horizontal + + Horizontal item sizing + Choose whether horizontal items use the full screen width, fit their content, or divide the available Gallery width into columns. + + Full width + Fit content + Fill columns + + diff --git a/packages/pluggableWidgets/gallery-native/src/__mocks__/@shopify/flash-list.tsx b/packages/pluggableWidgets/gallery-native/src/__mocks__/@shopify/flash-list.tsx index 841a291cf..f4ef120b5 100644 --- a/packages/pluggableWidgets/gallery-native/src/__mocks__/@shopify/flash-list.tsx +++ b/packages/pluggableWidgets/gallery-native/src/__mocks__/@shopify/flash-list.tsx @@ -2,7 +2,7 @@ import React from "react"; import { View } from "react-native"; // Mock FlashList - render items directly without using FlatList -export const FlashList = React.forwardRef((props: any) => { +export const FlashList = React.forwardRef((props: any, ref) => { const { data = [], renderItem, @@ -27,7 +27,7 @@ export const FlashList = React.forwardRef((props: any) => { }; return ( - + {ListHeaderComponent} {renderItems()} {ListFooterComponent} diff --git a/packages/pluggableWidgets/gallery-native/src/components/Gallery.tsx b/packages/pluggableWidgets/gallery-native/src/components/Gallery.tsx index 2caa0486e..a26be77a6 100644 --- a/packages/pluggableWidgets/gallery-native/src/components/Gallery.tsx +++ b/packages/pluggableWidgets/gallery-native/src/components/Gallery.tsx @@ -1,9 +1,18 @@ import { ReactElement, ReactNode, useCallback, useMemo, useState } from "react"; -import { Text, Pressable, View, ViewProps, Platform, TouchableOpacity, useWindowDimensions } from "react-native"; +import { + LayoutChangeEvent, + Text, + Pressable, + View, + ViewProps, + Platform, + TouchableOpacity, + useWindowDimensions +} from "react-native"; import { ObjectItem, DynamicValue } from "mendix"; import DeviceInfo from "react-native-device-info"; import { GalleryStyle } from "../ui/Styles"; -import { PaginationEnum, ScrollDirectionEnum } from "../../typings/GalleryProps"; +import { HorizontalItemSizingEnum, PaginationEnum, ScrollDirectionEnum } from "../../typings/GalleryProps"; import { isAvailable } from "@mendix/piw-utils-internal"; import { extractStyles } from "@mendix/pluggable-widgets-tools"; import { FlashList } from "@shopify/flash-list"; @@ -24,6 +33,7 @@ export interface GalleryProps { pullDown?: () => void; pullDownIsExecuting?: boolean; scrollDirection: ScrollDirectionEnum; + horizontalItemSizing: HorizontalItemSizingEnum; style: GalleryStyle; tabletColumns: number; } @@ -34,10 +44,22 @@ export const Gallery = (props: GalleryProps): ReactElem const firstItemId = props.items?.[0]?.id; const lastItemId = props.items?.[props.items.length - 1]?.id; const { name, style, itemRenderer } = props; - const { width, height: windowHeight } = useWindowDimensions(); + const { width: windowWidth, height: windowHeight } = useWindowDimensions(); // FlashList is virtualized and only renders items that fit within its own height, so it needs a // non-zero height. The list area is sized from FlashList's reported content height instead of flex. const [contentHeight, setContentHeight] = useState(0); + const [containerWidth, setContainerWidth] = useState(0); + const horizontalItemWidth = isScrollDirectionVertical + ? undefined + : props.horizontalItemSizing === "fullWidth" + ? windowWidth + : props.horizontalItemSizing === "fillColumns" && containerWidth > 0 + ? containerWidth / numColumns + : undefined; + + const onContainerLayout = useCallback((event: LayoutChangeEvent): void => { + setContainerWidth(event.nativeEvent.layout.width); + }, []); const onEndReached = (): void => { if (props.pagination === "virtualScrolling" && props.hasMoreItems) { @@ -48,9 +70,8 @@ export const Gallery = (props: GalleryProps): ReactElem const renderItem = useCallback( (item: { item: T }): ReactElement => itemRenderer((children, onPress) => { - const itemStyle = isScrollDirectionVertical ? undefined : { width }; const listItemWrapperProps: ViewProps = { - style: itemStyle, + style: horizontalItemWidth === undefined ? undefined : { width: horizontalItemWidth }, testID: `${name}-list-item-${item.item.id}` }; const renderListItemContent = ( @@ -74,8 +95,7 @@ export const Gallery = (props: GalleryProps): ReactElem }, item.item), [ itemRenderer, - isScrollDirectionVertical, - width, + horizontalItemWidth, name, style.listItem, style.firstItem, @@ -160,7 +180,13 @@ export const Gallery = (props: GalleryProps): ReactElem const listStyle = isScrollDirectionVertical ? [{ flex: 1 }, props.style.list] : props.style.list; return ( - + {props.filters ? {props.filters} : null} {!hasItems && renderEmptyPlaceholder} {hasItems ? ( diff --git a/packages/pluggableWidgets/gallery-native/src/components/__tests__/Gallery.spec.tsx b/packages/pluggableWidgets/gallery-native/src/components/__tests__/Gallery.spec.tsx index 36eeb3574..59901208f 100644 --- a/packages/pluggableWidgets/gallery-native/src/components/__tests__/Gallery.spec.tsx +++ b/packages/pluggableWidgets/gallery-native/src/components/__tests__/Gallery.spec.tsx @@ -30,6 +30,7 @@ const defaultProps: GalleryProps = { pagination: "virtualScrolling", phoneColumns: 2, scrollDirection: "vertical", + horizontalItemSizing: "fullWidth", style: { container: {} }, tabletColumns: 3 }; @@ -46,6 +47,39 @@ describe("Gallery", () => { expect(gallery).toMatchSnapshot(); }); + it("preserves full-width horizontal items by default", () => { + const gallery = render(); + const list = gallery.getByTestId("gallery-test-list"); + const firstItem = gallery.getByTestId("gallery-test-list-item-11"); + + expect(list.props.ItemSeparatorComponent).toBeUndefined(); + expect(firstItem.props.style).toEqual({ width: 750 }); + }); + + it("does not constrain or separate fit-content horizontal items", () => { + const gallery = render( + + ); + const list = gallery.getByTestId("gallery-test-list"); + const firstItem = gallery.getByTestId("gallery-test-list-item-11"); + + expect(list.props.ItemSeparatorComponent).toBeUndefined(); + expect(firstItem.props.style).toBeUndefined(); + }); + + it("divides the available width between horizontal columns", () => { + const gallery = render( + + ); + + fireEvent(gallery.getByTestId("gallery-test"), "layout", { + nativeEvent: { layout: { width: 750, height: 100, x: 0, y: 0 } } + }); + + expect(gallery.getByTestId("gallery-test-list-item-11").props.style).toEqual({ width: 375 }); + expect(gallery.getByTestId("gallery-test-list").props.ItemSeparatorComponent).toBeUndefined(); + }); + it("renders correctly with empty list and custom placeholder", () => { const gallery = render( - + diff --git a/packages/pluggableWidgets/gallery-native/typings/GalleryProps.d.ts b/packages/pluggableWidgets/gallery-native/typings/GalleryProps.d.ts index 150fad262..e92036e64 100644 --- a/packages/pluggableWidgets/gallery-native/typings/GalleryProps.d.ts +++ b/packages/pluggableWidgets/gallery-native/typings/GalleryProps.d.ts @@ -9,6 +9,8 @@ import { Big } from "big.js"; export type ScrollDirectionEnum = "vertical" | "horizontal"; +export type HorizontalItemSizingEnum = "fullWidth" | "fitContent" | "fillColumns"; + export type PaginationEnum = "virtualScrolling" | "buttons"; export interface FilterListType { @@ -25,6 +27,7 @@ export interface GalleryProps