Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/pluggableWidgets/gallery-native/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

## [Unreleased]

### Changed

- We migrated from using the native FlatList to @shopify/flash-list.

## [2.0.2] - 2025-10-17

### Fixed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,13 @@ appId: "${APP_ID}"
direction: UP
- assertVisible:
text: ".*Title 9.*"
- tapOn:
text: "Load more"
- assertVisible:
text: ".*Title 10.*"
- swipe:
from:
text: ".*Title 10.*"
direction: UP
- assertVisible:
text: ".*Title 14.*"
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
appId: "${APP_ID}"
---
- runFlow:
file: "../../../../../../maestro/Precondition.yaml"
- tapOn:
text: "G"
- tapOn:
text: "Gallery"
- tapOn:
text: "Gallery"
- assertVisible:
text: ".*Title 0.*"
- repeat:
times: 25
commands:
- swipe:
start: 90%, 10%
end: 15%, 10%
- assertVisible:
text: ".*Title 20.*"
3 changes: 2 additions & 1 deletion packages/pluggableWidgets/gallery-native/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "gallery-native",
"widgetName": "Gallery",
"version": "2.0.2",
"version": "2.1.0",
"description": "A flexible gallery widget that renders columns, rows and layouts.",
"copyright": "© Mendix Technology BV 2022. All rights reserved.",
"license": "Apache-2.0",
Expand All @@ -23,6 +23,7 @@
},
"dependencies": {
"@mendix/piw-utils-internal": "*",
"@shopify/flash-list": "2.2.2",
"react-native-device-info": "14.0.4"
},
"devDependencies": {
Expand Down
1 change: 1 addition & 0 deletions packages/pluggableWidgets/gallery-native/src/Gallery.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export const Gallery = (props: GalleryProps<GalleryStyle>): ReactElement => {
}),
{}
),
// eslint-disable-next-line react-hooks/exhaustive-deps
[props.filterList, viewStateFilters.current]
);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import React from "react";
import { View } from "react-native";

// Mock FlashList - render items directly without using FlatList
export const FlashList = React.forwardRef((props: any) => {
const {
data = [],
renderItem,
ListEmptyComponent,
ListFooterComponent,
ListHeaderComponent,
onRefresh,
...rest
} = props;

// simulate the refreshControl structure returned by real FlashList
const refreshControl = onRefresh ? { props: { onRefresh } } : undefined;

const renderItems = (): any => {
if (!data || data.length === 0) {
return ListEmptyComponent;
}

return data.map((item: any, index: number) => (
<View key={item.id || index}>{renderItem?.({ item, index })}</View>
));
};

return (
<View {...rest} refreshControl={refreshControl}>
{ListHeaderComponent}
{renderItems()}
{ListFooterComponent}
</View>
);
});

FlashList.displayName = "FlashList";
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { ReactElement, ReactNode, useCallback } from "react";
import { Text, FlatList, Pressable, View, ViewProps, Platform, TouchableOpacity } from "react-native";
import { ReactElement, ReactNode, useCallback, useMemo } from "react";
import { 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 { isAvailable } from "@mendix/piw-utils-internal";
import { extractStyles } from "@mendix/pluggable-widgets-tools";
import { FlashList } from "@shopify/flash-list";

const DEFAULT_RIPPLE_COLOR = "rgba(0, 0, 0, 0.2)";

Expand Down Expand Up @@ -33,6 +34,7 @@ export const Gallery = <T extends ObjectItem>(props: GalleryProps<T>): ReactElem
const firstItemId = props.items?.[0]?.id;
const lastItemId = props.items?.[props.items.length - 1]?.id;
const { name, style, itemRenderer } = props;
const { width } = useWindowDimensions();

const onEndReached = (): void => {
if (props.pagination === "virtualScrolling" && props.hasMoreItems) {
Expand All @@ -43,8 +45,9 @@ export const Gallery = <T extends ObjectItem>(props: GalleryProps<T>): ReactElem
const renderItem = useCallback(
(item: { item: T }): ReactElement =>
itemRenderer((children, onPress) => {
const itemStyle = isScrollDirectionVertical ? undefined : { width };
const listItemWrapperProps: ViewProps = {
style: isScrollDirectionVertical && { width: `${100 / numColumns}%` },
style: itemStyle,
testID: `${name}-list-item-${item.item.id}`
};
const renderListItemContent = (
Expand All @@ -67,9 +70,9 @@ export const Gallery = <T extends ObjectItem>(props: GalleryProps<T>): ReactElem
);
}, item.item),
[
isScrollDirectionVertical,
numColumns,
itemRenderer,
isScrollDirectionVertical,
width,
name,
style.listItem,
style.firstItem,
Expand All @@ -79,7 +82,7 @@ export const Gallery = <T extends ObjectItem>(props: GalleryProps<T>): ReactElem
]
);

const loadMoreButton = (): ReactElement | null => {
const loadMoreButton = useMemo((): ReactElement | null => {
const renderButton = (
<Text style={props.style.loadMoreButtonCaption}>
{props.loadMoreButtonCaption && isAvailable(props.loadMoreButtonCaption)
Expand Down Expand Up @@ -118,16 +121,26 @@ export const Gallery = <T extends ObjectItem>(props: GalleryProps<T>): ReactElem
<TouchableOpacity {...buttonProps}>{renderButton}</TouchableOpacity>
)
) : null;
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
props.style.loadMoreButtonCaption,
props.loadMoreButtonCaption,
props.style.loadMoreButtonPressableContainer,
name,
props.pagination,
props.hasMoreItems,
props.loadMoreItems
]);

const renderEmptyPlaceholder = (): ReactElement => (
<View style={props.style.emptyPlaceholder}>{props.emptyPlaceholder}</View>
const renderEmptyPlaceholder = useMemo(
(): ReactElement => <View style={props.style.emptyPlaceholder}>{props.emptyPlaceholder}</View>,
[props.style.emptyPlaceholder, props.emptyPlaceholder]
);

return (
<View testID={`${name}`} style={props.style.container}>
{props.filters ? <View>{props.filters}</View> : null}
<FlatList
<FlashList
{...(isScrollDirectionVertical && props.pullDown ? { onRefresh: props.pullDown } : {})}
{...(isScrollDirectionVertical ? { numColumns } : {})}
ListFooterComponent={loadMoreButton}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { render, fireEvent, act } from "@testing-library/react-native";
import { Gallery, GalleryProps } from "../Gallery";

jest.mock("react-native-device-info", () => ({ isTablet: jest.fn().mockReturnValue(false) }));
jest.mock("@shopify/flash-list"); // Mocking FlashList API because it causes issues with the test renderer

const itemWrapperFunction =
({ onClick }: { onClick?: () => void }): GalleryProps<ObjectItem>["itemRenderer"] =>
Expand Down
Loading
Loading