react-native-dynamic-shimmer
Version:
Shimmer component wrapper for React Native apps
304 lines • 16.3 kB
JavaScript
import React from 'react';
import { FlatList, View, } from 'react-native';
import { marginProperties } from '../../constants';
import { width as screenWidth } from '../../theme';
import { calculateAdjustedChildWidth, calculateAspectRatioDimensions, calculateGlobalScreenWidth, calculateParentWidth, calculateWidthFromAspectRatio, calculatedEachChildWidth, checkRootComponent, computeTotalWidths, distributeWidths, flattenStyle, getAdjustedWidth, getAriaLabelWidth, getHorizontalMargin, getWidthFromAspectRatio, isAspectRatio, } from '../../utils';
import shimmerStyles from './ShimmerStyles';
import { RenderShimmerView, ShimmerFlatList } from './components';
/**
* Shimmer component that wraps child components and displays a loading state.
*
* @param {Object} props - The props for the Shimmer component.
* @param {React.ReactNode} props.children - The child components to render.
* @param {boolean} props.loading - Indicates if the shimmer is in loading state.
* @param {React.ReactElement} props.shimmerElement - The gradient element used for the shimmer effect.
* @param {number} props.duration - The duration of the shimmer effect animation
* @returns {React.ReactNode} - The rendered children or shimmer view.
*/
const Shimmer = ({ children, loading, shimmerElement, duration, }) => {
if (!loading)
return children;
/**
* Recursively fetches and renders leaf nodes from a given set of children,
* applying appropriate width and margin calculations to ensure proper layout
* within the parent component's dimensions. This function supports handling
* of aspect ratios, margin adjustments, and shimmer effects for loading states.
*
* @param {Object} params - Parameters for fetching leaf nodes.
* @param {React.ReactNode} params.subChildren - The child components or elements
* that are to be processed and rendered. Can be an array of elements or a single element.
* @param {Object} params.parentInfo - Information about the parent component,
* including a reference to the parent element.
* @param {number} params.calculatedWidth - The width that should be assigned to
* the child components based on the parent's dimensions and other factors.
* @param {boolean} params.isPreviousRow - Indicates if the current node is
* part of a previous row, affecting how width is calculated.
* @param {boolean} params.isRoot - Indicates if the current component is
* the root of the rendering tree.
* @param {number} params.prevMargin - The margin that was applied to the
* previous child, which may affect the layout of the current child.
*
* @returns {React.ReactNode} - The rendered child components with appropriate
* width and margin adjustments applied. This can include shimmer effects
* for loading states, as well as nested child components processed recursively.
*
*/
const fetchLeafNodes = ({ subChildren, parentInfo, calculatedWidth, isPreviousRow, isRoot, prevMargin, }) => {
// Initialize total margin for child components.
let finalMargin = 0;
// Store child components with calculated widths.
let widthData = [];
// Calculate global screen width.
const { globalScreenWidth } = calculateGlobalScreenWidth(parentInfo);
// Extract parent component from parentInfo.
const parent = parentInfo?.parent;
// Check if the parent is the root component.
const parentIsRoot = checkRootComponent(parent);
// Count number of child components.
const childrenCount = parent?.props?.children?.length || 0;
// Flatten parent's styles.
const flattenedParentStyle = flattenStyle(parent?.props?.style);
// Check if parent layout is 'row'.
const isRow = flattenedParentStyle?.flexDirection === 'row';
// Determine if current context is a root row.
const isRootRow = isRoot && !isRow;
// Flatten content container styles.
const flattenContentContainerStyle = flattenStyle(parent?.props?.contentContainerStyle);
const { widthAccordingToAspectRatio } = calculateWidthFromAspectRatio(flattenedParentStyle);
const { aspectRatioWidth } = calculateAspectRatioDimensions(flattenContentContainerStyle, calculatedWidth);
const primaryWidth = getWidthFromAspectRatio({
isAspectRatioBased: isAspectRatio(flattenedParentStyle, 'width'),
aspectRatioValue: widthAccordingToAspectRatio,
flattenedStyleValue: flattenedParentStyle?.width,
});
const contentContainerWidth = getWidthFromAspectRatio({
isAspectRatioBased: isAspectRatio(flattenContentContainerStyle, 'width'),
aspectRatioValue: aspectRatioWidth,
flattenedStyleValue: flattenContentContainerStyle?.width,
});
const baseWidth = primaryWidth || contentContainerWidth || calculatedWidth || screenWidth;
const parentWidth = calculateParentWidth({
baseWidth: baseWidth,
containerWidth: screenWidth,
parentIsRoot: parentIsRoot,
calculatedChildWidth: calculatedWidth,
});
const { totalWidthWithFixedChildren, totalChildrenWithoutWidth, } = computeTotalWidths(subChildren, parentWidth);
const remainingWidth = parentWidth - globalScreenWidth - totalWidthWithFixedChildren;
if (childrenCount > 0) {
widthData = distributeWidths(parent?.props?.children, parentWidth, flattenedParentStyle);
}
const childrenWithoutWidth = childrenCount - totalChildrenWithoutWidth;
const divisor = isRootRow
? 1
: childrenCount > 0
? childrenCount - childrenWithoutWidth
: 1;
const availableWidth = isRootRow
? parentWidth - globalScreenWidth
: remainingWidth;
const adjustedChildWidth = calculateAdjustedChildWidth(subChildren, parentInfo, parentWidth);
const computedWidth = calculatedWidth && !isRow
? calculatedWidth - globalScreenWidth
: availableWidth;
const derivedWidth = calculatedWidth
? calculatedWidth - globalScreenWidth
: undefined;
const calculatedChildWidth = isPreviousRow
? derivedWidth
: adjustedChildWidth
? adjustedChildWidth - globalScreenWidth
: computedWidth / divisor;
/**
* Calculates the total margin from a given style object.
*
* This function iterates through a predefined set of margin properties and
* accumulates their values from the provided style. It handles the case where
* margin values might be undefined, ensuring that only valid numeric margins
* are included in the total.
*
* @param style - The style object from which to extract margin values. It
* should conform to the ViewStyle type.
* @returns The total margin as a number. If no valid margin properties
* are found, it returns 0.
*/
const calculateTotalMargin = (style) => {
marginProperties.forEach(marginProp => {
if (style?.[marginProp] !== undefined) {
finalMargin += Number(style?.[marginProp]) ?? 0;
}
});
return finalMargin;
};
return React.Children.map(subChildren, (child, index) => {
const childStyle = flattenStyle(child?.props?.style);
const totalMargin = getHorizontalMargin(childStyle);
const { heightAccordingToAspectRatio } = calculateWidthFromAspectRatio(flattenedParentStyle, calculatedChildWidth);
const { aspectRatioHeight } = calculateAspectRatioDimensions(flattenContentContainerStyle, calculatedChildWidth);
const parentHeightFromAspectRatio = getWidthFromAspectRatio({
isAspectRatioBased: isAspectRatio(flattenedParentStyle, 'height'),
aspectRatioValue: heightAccordingToAspectRatio,
flattenedStyleValue: flattenedParentStyle?.height,
});
const contentContainerHeightFromAspectRatio = getWidthFromAspectRatio({
isAspectRatioBased: isAspectRatio(flattenContentContainerStyle, 'height'),
aspectRatioValue: aspectRatioHeight,
flattenedStyleValue: flattenContentContainerStyle?.height,
});
const { shimmerStylesForEachChild } = shimmerStyles({
child,
numericWidth: getAdjustedWidth(calculatedChildWidth, totalMargin, prevMargin),
parentHeight: parentHeightFromAspectRatio || contentContainerHeightFromAspectRatio,
parentWidth: parentWidth,
});
/**
* Processes a React component to apply a shimmer effect.
*
* This function checks the type of the provided React element and applies
* the shimmer effect conditionally based on whether the element is a
* FlatList, a custom React component, or a nested child. It calculates
* necessary widths and margins for rendering and handles various component
* types accordingly.
*
* @param element - The React element (JSX.Element) to process. This can be
* a FlatList, a custom component, or any nested child element.
* @returns The processed React element with a shimmer effect applied.
* If the element is a FlatList, it renders using the `renderFlatList`
* function; otherwise, it clones and modifies the element as needed.
*/
const processComponentWithShimmer = (element) => {
const renderFlatList = (nestedChild) => ShimmerFlatList({
child: nestedChild,
widthData,
calculatedChildWidth,
globalScreenWidth,
fetchLeafNodes,
isRow,
parentInfo,
finalMargin,
});
if (element?.type === FlatList) {
return renderFlatList(element);
}
else if (element?.type?.prototype?.isReactComponent) {
const widthForEachChild = calculatedEachChildWidth({
child: element,
...{ widthData, calculatedChildWidth, globalScreenWidth },
});
return React.cloneElement(element, {
children: fetchLeafNodes({
subChildren: element?.props?.children,
parentInfo: {
parent: element,
},
calculatedWidth: widthForEachChild,
isPreviousRow: isRow,
isRoot: parentInfo?.parent,
prevMargin: finalMargin,
}),
});
}
else {
const nestedChildren = element?.type?.(element?.props);
if (nestedChildren?.type?.name) {
return processComponentWithShimmer(nestedChildren);
}
if (nestedChildren?.type === FlatList) {
return renderFlatList(nestedChildren);
}
if (typeof nestedChildren?.props?.children === 'string' ||
(!nestedChildren?.type?.name && !nestedChildren?.props?.children)) {
const nestedChildStyle = flattenStyle(nestedChildren?.props?.style);
calculateTotalMargin(nestedChildStyle);
const ariaLabelWidth = getAriaLabelWidth(nestedChildren, nestedChildStyle);
return RenderShimmerView({
index,
childStyle: nestedChildStyle,
shimmerStyle: shimmerStylesForEachChild,
shimmerWidth: getAdjustedWidth(ariaLabelWidth ? ariaLabelWidth : calculatedChildWidth, totalMargin, prevMargin),
parentWidth,
loading,
shimmerElement,
duration,
});
}
else {
const widthForEachChild = calculatedEachChildWidth({
child: nestedChildren,
...{ widthData, calculatedChildWidth, globalScreenWidth },
});
return React.cloneElement(nestedChildren, {
children: fetchLeafNodes({
subChildren: nestedChildren?.props?.children,
parentInfo: {
parent: nestedChildren,
},
calculatedWidth: widthForEachChild,
isPreviousRow: isRow,
isRoot: parentInfo?.parent,
prevMargin: finalMargin,
}),
});
}
}
};
if (child?.type?.name) {
return processComponentWithShimmer(child);
}
else {
if (typeof child?.props?.children === 'string') {
calculateTotalMargin(childStyle);
const ariaLabelWidth = getAriaLabelWidth(child, childStyle);
return RenderShimmerView({
index,
childStyle,
shimmerStyle: shimmerStylesForEachChild,
shimmerWidth: getAdjustedWidth(ariaLabelWidth ? ariaLabelWidth : calculatedChildWidth, totalMargin, prevMargin),
parentWidth,
loading,
shimmerElement,
duration,
});
}
if (!child?.props?.children) {
calculateTotalMargin(childStyle || {});
const ariaLabelWidth = getAriaLabelWidth(child, childStyle);
return RenderShimmerView({
index,
childStyle,
shimmerStyle: shimmerStylesForEachChild,
shimmerWidth: getAdjustedWidth(ariaLabelWidth ? ariaLabelWidth : calculatedChildWidth, totalMargin, prevMargin),
parentWidth,
loading,
shimmerElement,
duration,
});
}
else {
const widthForEachChild = calculatedEachChildWidth({
...{ child, widthData, calculatedChildWidth, globalScreenWidth },
});
return React.cloneElement(child, {
children: fetchLeafNodes({
subChildren: child?.props?.children,
parentInfo: {
parent: child,
},
calculatedWidth: widthForEachChild,
isPreviousRow: isRow,
isRoot: parentInfo?.parent,
prevMargin: finalMargin,
}),
});
}
}
});
};
const shimmerChildren = fetchLeafNodes({
subChildren: children,
parentInfo: { parent: React.createElement(React.Fragment, null) },
});
return (React.createElement(View, { pointerEvents: loading ? 'none' : 'auto' }, shimmerChildren));
};
export default Shimmer;
//# sourceMappingURL=Shimmer.js.map