diff --git a/app/client/src/layoutSystems/anvil/common/AnvilFlexComponent.tsx b/app/client/src/layoutSystems/anvil/common/AnvilFlexComponent.tsx index e62650358e..e74af5aa01 100644 --- a/app/client/src/layoutSystems/anvil/common/AnvilFlexComponent.tsx +++ b/app/client/src/layoutSystems/anvil/common/AnvilFlexComponent.tsx @@ -68,6 +68,7 @@ export const AnvilFlexComponent = forwardRef( flexBasis: isFillWidget ? "0%" : "auto", padding: "spacing-1", alignItems: "center", + width: "max-content", }; if (widgetSize) { const { maxHeight, maxWidth, minHeight, minWidth } = widgetSize; diff --git a/app/client/src/layoutSystems/anvil/integrations/layoutSelectors.ts b/app/client/src/layoutSystems/anvil/integrations/layoutSelectors.ts new file mode 100644 index 0000000000..c4ad61945b --- /dev/null +++ b/app/client/src/layoutSystems/anvil/integrations/layoutSelectors.ts @@ -0,0 +1,36 @@ +import { getLayoutElementPositions } from "layoutSystems/common/selectors"; +import type { + LayoutElementPosition, + LayoutElementPositions, +} from "layoutSystems/common/types"; +import { createSelector } from "reselect"; +import { AlignmentIndexMap } from "../utils/constants"; +import { FlexLayerAlignment } from "layoutSystems/common/utils/constants"; + +export const ALIGNMENT_WIDTH_THRESHOLD = 0.95; + +export function shouldOverrideAlignmentStyle(layoutId: string) { + return createSelector( + getLayoutElementPositions, + (positions: LayoutElementPositions): boolean => { + if (!layoutId || !positions || !positions[layoutId]) return false; + + // If positions don't exist for start alignment, return false as this layout is not aligned. + if (!positions[`${layoutId}-0`]) return false; + + const layoutPosition: LayoutElementPosition = positions[layoutId]; + const threshold = layoutPosition.width * ALIGNMENT_WIDTH_THRESHOLD; + + // return true if width of any alignment exceeds the limit. + return [ + FlexLayerAlignment.Start, + FlexLayerAlignment.Center, + FlexLayerAlignment.End, + ].some((each: FlexLayerAlignment) => { + const alignmentPosition: LayoutElementPosition = + positions[`${layoutId}-${AlignmentIndexMap[each]}`]; + return alignmentPosition.width >= threshold; + }); + }, + ); +} diff --git a/app/client/src/layoutSystems/anvil/layoutComponents/components/alignedWidgetRow/AlignedWidgetRowComp.tsx b/app/client/src/layoutSystems/anvil/layoutComponents/components/alignedWidgetRow/AlignedWidgetRowComp.tsx new file mode 100644 index 0000000000..a4e9905e38 --- /dev/null +++ b/app/client/src/layoutSystems/anvil/layoutComponents/components/alignedWidgetRow/AlignedWidgetRowComp.tsx @@ -0,0 +1,194 @@ +import React, { useEffect, useMemo, useState } from "react"; +import { + LayoutComponentTypes, + type LayoutComponentProps, + type WidgetLayoutProps, +} from "layoutSystems/anvil/utils/anvilTypes"; +import { + AlignmentIndexMap, + MOBILE_BREAKPOINT, +} from "layoutSystems/anvil/utils/constants"; +import { FlexLayerAlignment } from "layoutSystems/common/utils/constants"; +import { renderWidgets } from "layoutSystems/anvil/utils/layouts/renderUtils"; +import { FlexLayout, type FlexLayoutProps } from "../FlexLayout"; +import { isFillWidgetPresentInList } from "layoutSystems/anvil/utils/layouts/widgetUtils"; +import { getAnvilLayoutDOMId } from "layoutSystems/common/utils/LayoutElementPositionsObserver/utils"; +import { + ALIGNMENT_WIDTH_THRESHOLD, + shouldOverrideAlignmentStyle, +} from "layoutSystems/anvil/integrations/layoutSelectors"; +import { useSelector } from "react-redux"; +import { RenderModes } from "constants/WidgetConstants"; + +/** + * If AlignedRow hasFillWidget: + * then render all children directly within the AlignedRow (row / flex-start / wrap); + * no need for alignments. + * + * Else: + * render children in 3 alignments: start, center and end. + * Each alignment has following characteristics: + * 1. Mobile viewport: + * - flex-wrap: wrap. + * - flex-basis: auto. + * ~ This ensures the alignment takes up as much space as needed by the children. + * ~ It can stretch to the full width of the viewport. + * ~ or collapse completely if there is no content. + * + * 2. Larger view ports: + * - flex-wrap: nowrap. + * - flex-basis: 0%. + * ~ This ensures that alignments share the total space equally, until possible. + * ~ Soon as the content in any alignment needs more space, it will wrap to the next line + * thanks to flex wrap in the parent layout. + */ +const AlignedWidgetRowComp = (props: LayoutComponentProps) => { + const { canvasId, layout, layoutId, renderMode } = props; + // Whether default alignment styles should be overridden, when renderMode = Canvas. + const shouldOverrideStyle: boolean = useSelector( + shouldOverrideAlignmentStyle(layoutId), + ); + + // check if layout renders a Fill widget. + const hasFillWidget: boolean = isFillWidgetPresentInList( + layout as WidgetLayoutProps[], + ); + + const [isAnyAlignmentOverflowing, setIsAnyAlignmentOverflowing] = + useState(false); + + useEffect(() => { + // getBoundingClientRect is an expensive operation and should only be used when renderMode = Page, + // because layout positions are not available in that case. + if (hasFillWidget || renderMode !== RenderModes.PAGE) return; + const parentLayoutId = getAnvilLayoutDOMId(canvasId, layoutId); + const parentLayout = document.getElementById(parentLayoutId); + if (parentLayout) { + const parentLayoutWidth = parentLayout.getBoundingClientRect().width; + + // Use requestAnimationFrame to ensure calculation is done after rendering + requestAnimationFrame(() => { + const isOverflowing = [ + FlexLayerAlignment.Start, + FlexLayerAlignment.Center, + FlexLayerAlignment.End, + ].some((each: FlexLayerAlignment) => { + const alignmentId = `${parentLayoutId}-${AlignmentIndexMap[each]}`; + const alignment = document.getElementById(alignmentId); + if (!alignment) return false; + const alignmentWidth = alignment.getBoundingClientRect().width; + // return true if width of any alignment exceeds the limit. + return ( + alignmentWidth >= parentLayoutWidth * ALIGNMENT_WIDTH_THRESHOLD + ); + }); + setIsAnyAlignmentOverflowing(isOverflowing); + }); + } + }, [hasFillWidget, layout.length, renderMode]); + + useEffect(() => { + if (hasFillWidget || renderMode === RenderModes.PAGE) return; + setIsAnyAlignmentOverflowing(shouldOverrideStyle); + }, [hasFillWidget, renderMode, shouldOverrideStyle]); + + const commonProps: Omit< + FlexLayoutProps, + "children" | "layoutId" | "layoutIndex" + > = useMemo(() => { + return { + alignSelf: "stretch", + canvasId, + direction: "row", + flexBasis: isAnyAlignmentOverflowing + ? { base: "auto" } + : { base: "auto", [`${MOBILE_BREAKPOINT}px`]: "0%" }, + flexGrow: 1, + flexShrink: 1, + layoutType: LayoutComponentTypes.WIDGET_ROW, + parentDropTarget: props.parentDropTarget, + renderMode: props.renderMode, + wrap: isAnyAlignmentOverflowing + ? { base: "wrap" } + : { base: "wrap", [`${MOBILE_BREAKPOINT}px`]: "nowrap" }, + className: props.className, + }; + }, [isAnyAlignmentOverflowing]); + + // If a Fill widget exists, then render the child widgets together. + if (hasFillWidget) { + return <>{renderWidgets(props)}; + } + + /** + * else render the child widgets separately + * in their respective alignments. + */ + const startChildren: WidgetLayoutProps[] = ( + layout as WidgetLayoutProps[] + ).filter( + (each: WidgetLayoutProps) => each.alignment === FlexLayerAlignment.Start, + ); + const centerChildren: WidgetLayoutProps[] = ( + layout as WidgetLayoutProps[] + ).filter( + (each: WidgetLayoutProps) => each.alignment === FlexLayerAlignment.Center, + ); + const endChildren: WidgetLayoutProps[] = ( + layout as WidgetLayoutProps[] + ).filter( + (each: WidgetLayoutProps) => each.alignment === FlexLayerAlignment.End, + ); + + // TODO: After positionObserver integration, + // check if use of FlexLayout is causing performance or other issues. + // WDS Flex can be used as a replacement. + return ( + <> + + {renderWidgets({ + ...props, + layout: startChildren, + })} + + + {renderWidgets( + { + ...props, + layout: centerChildren, + }, + startChildren?.length, + )} + + + {renderWidgets( + { + ...props, + layout: endChildren, + }, + startChildren?.length + centerChildren?.length, + )} + + + ); +}; + +export default AlignedWidgetRowComp; diff --git a/app/client/src/layoutSystems/anvil/layoutComponents/components/AlignedWidgetRow.tsx b/app/client/src/layoutSystems/anvil/layoutComponents/components/alignedWidgetRow/index.tsx similarity index 75% rename from app/client/src/layoutSystems/anvil/layoutComponents/components/AlignedWidgetRow.tsx rename to app/client/src/layoutSystems/anvil/layoutComponents/components/alignedWidgetRow/index.tsx index f1e9713019..50abb014c2 100644 --- a/app/client/src/layoutSystems/anvil/layoutComponents/components/AlignedWidgetRow.tsx +++ b/app/client/src/layoutSystems/anvil/layoutComponents/components/alignedWidgetRow/index.tsx @@ -1,11 +1,12 @@ -import BaseLayoutComponent from "../BaseLayoutComponent"; +import React from "react"; +import BaseLayoutComponent from "../../BaseLayoutComponent"; import { type DeriveHighlightsFn, LayoutComponentTypes, } from "layoutSystems/anvil/utils/anvilTypes"; -import type { FlexLayoutProps } from "./FlexLayout"; +import type { FlexLayoutProps } from "../FlexLayout"; import { deriveAlignedRowHighlights } from "layoutSystems/anvil/utils/layouts/highlights/alignedRowHighlights"; -import { renderWidgetsInAlignedRow } from "layoutSystems/anvil/utils/layouts/renderUtils"; +import AlignedWidgetRowComp from "./AlignedWidgetRowComp"; class AlignedWidgetRow extends BaseLayoutComponent { static type: LayoutComponentTypes = LayoutComponentTypes.ALIGNED_WIDGET_ROW; @@ -13,7 +14,7 @@ class AlignedWidgetRow extends BaseLayoutComponent { static deriveHighlights: DeriveHighlightsFn = deriveAlignedRowHighlights; renderChildWidgets(): React.ReactNode { - return renderWidgetsInAlignedRow(this.props); + return ; } static rendersWidgets: boolean = true; diff --git a/app/client/src/layoutSystems/anvil/utils/layouts/highlights/alignedRowHighlights.test.ts b/app/client/src/layoutSystems/anvil/utils/layouts/highlights/alignedRowHighlights.test.ts index cb3711d848..6f8c08fb3d 100644 --- a/app/client/src/layoutSystems/anvil/utils/layouts/highlights/alignedRowHighlights.test.ts +++ b/app/client/src/layoutSystems/anvil/utils/layouts/highlights/alignedRowHighlights.test.ts @@ -11,7 +11,7 @@ import { } from "layoutSystems/common/utils/constants"; import { HIGHLIGHT_SIZE } from "../../constants"; import LayoutFactory from "layoutSystems/anvil/layoutComponents/LayoutFactory"; -import AlignedWidgetRow from "layoutSystems/anvil/layoutComponents/components/AlignedWidgetRow"; +import AlignedWidgetRow from "layoutSystems/anvil/layoutComponents/components/alignedWidgetRow"; import type { BaseWidgetProps } from "widgets/BaseWidgetHOC/withBaseWidgetHOC"; import { mockButtonProps } from "mocks/widgetProps/button"; import { getAlignmentLayoutId } from "../layoutUtils"; diff --git a/app/client/src/layoutSystems/anvil/utils/layouts/layoutUtils.ts b/app/client/src/layoutSystems/anvil/utils/layouts/layoutUtils.ts index 7c2df6f0fd..71658d8dd3 100644 --- a/app/client/src/layoutSystems/anvil/utils/layouts/layoutUtils.ts +++ b/app/client/src/layoutSystems/anvil/utils/layouts/layoutUtils.ts @@ -9,7 +9,7 @@ import type { FlexLayerAlignment } from "layoutSystems/common/utils/constants"; import { AlignmentIndexMap } from "../constants"; import AlignedLayoutColumn from "layoutSystems/anvil/layoutComponents/components/AlignedLayoutColumn"; import AlignedWidgetColumn from "layoutSystems/anvil/layoutComponents/components/AlignedWidgetColumn"; -import AlignedWidgetRow from "layoutSystems/anvil/layoutComponents/components/AlignedWidgetRow"; +import AlignedWidgetRow from "layoutSystems/anvil/layoutComponents/components/alignedWidgetRow"; import LayoutColumn from "layoutSystems/anvil/layoutComponents/components/LayoutColumn"; import LayoutRow from "layoutSystems/anvil/layoutComponents/components/LayoutRow"; import WidgetColumn from "layoutSystems/anvil/layoutComponents/components/WidgetColumn"; diff --git a/app/client/src/layoutSystems/anvil/utils/layouts/renderUtils.tsx b/app/client/src/layoutSystems/anvil/utils/layouts/renderUtils.tsx index bba5a3c9ea..77adea5a24 100644 --- a/app/client/src/layoutSystems/anvil/utils/layouts/renderUtils.tsx +++ b/app/client/src/layoutSystems/anvil/utils/layouts/renderUtils.tsx @@ -1,19 +1,11 @@ import React, { type ReactNode } from "react"; import LayoutFactory from "layoutSystems/anvil/layoutComponents/LayoutFactory"; -import { - LayoutComponentTypes, - type LayoutComponentProps, - type LayoutProps, - type WidgetLayoutProps, +import type { + LayoutComponentProps, + LayoutProps, + WidgetLayoutProps, } from "../anvilTypes"; import { type RenderMode, RenderModes } from "constants/WidgetConstants"; -import { isFillWidgetPresentInList } from "./widgetUtils"; -import { AlignmentIndexMap, MOBILE_BREAKPOINT } from "../constants"; -import { - FlexLayout, - type FlexLayoutProps, -} from "layoutSystems/anvil/layoutComponents/components/FlexLayout"; -import { FlexLayerAlignment } from "layoutSystems/common/utils/constants"; import type BaseLayoutComponent from "layoutSystems/anvil/layoutComponents/BaseLayoutComponent"; import { WidgetRenderer } from "layoutSystems/anvil/layoutComponents/WidgetRenderer"; @@ -78,125 +70,3 @@ export function renderLayouts( ); }); } - -/** - * If AlignedRow hasFillWidget: - * then render all children directly within the AlignedRow (row / flex-start / wrap); - * no need for alignments. - * - * Else: - * render children in 3 alignments: start, center and end. - * Each alignment has following characteristics: - * 1. Mobile viewport: - * - flex-wrap: wrap. - * - flex-basis: auto. - * ~ This ensures the alignment takes up as much space as needed by the children. - * ~ It can stretch to the full width of the viewport. - * ~ or collapse completely if there is no content. - * - * 2. Larger view ports: - * - flex-wrap: nowrap. - * - flex-basis: 0%. - * ~ This ensures that alignments share the total space equally, until possible. - * ~ Soon as the content in any alignment needs more space, it will wrap to the next line - * thanks to flex wrap in the parent layout. - */ -export function renderWidgetsInAlignedRow( - props: LayoutComponentProps, -): React.ReactNode { - const { canvasId, layout, layoutId } = props; - // check if layout renders a Fill widget. - const hasFillWidget: boolean = isFillWidgetPresentInList( - layout as WidgetLayoutProps[], - ); - - // If a Fill widget exists, then render the child widgets together. - if (hasFillWidget) { - return renderWidgets(props); - } - - /** - * else render the child widgets separately - * in their respective alignments. - */ - const commonProps: Omit< - FlexLayoutProps, - "children" | "layoutId" | "layoutIndex" - > = { - alignSelf: "stretch", - canvasId, - direction: "row", - flexBasis: { base: "auto", [`${MOBILE_BREAKPOINT}px`]: "0%" }, - flexGrow: 1, - flexShrink: 1, - layoutType: LayoutComponentTypes.WIDGET_ROW, - parentDropTarget: props.parentDropTarget, - renderMode: props.renderMode, - wrap: { base: "wrap", [`${MOBILE_BREAKPOINT}px`]: "nowrap" }, - className: props.className, - }; - - const startChildren: WidgetLayoutProps[] = ( - layout as WidgetLayoutProps[] - ).filter( - (each: WidgetLayoutProps) => each.alignment === FlexLayerAlignment.Start, - ); - const centerChildren: WidgetLayoutProps[] = ( - layout as WidgetLayoutProps[] - ).filter( - (each: WidgetLayoutProps) => each.alignment === FlexLayerAlignment.Center, - ); - const endChildren: WidgetLayoutProps[] = ( - layout as WidgetLayoutProps[] - ).filter( - (each: WidgetLayoutProps) => each.alignment === FlexLayerAlignment.End, - ); - - // TODO: After positionObserver integration, - // check if use of FlexLayout is causing performance or other issues. - // WDS Flex can be used as a replacement. - return [ - - {renderWidgets({ - ...props, - layout: startChildren, - })} - , - - {renderWidgets( - { - ...props, - layout: centerChildren, - }, - startChildren?.length, - )} - , - - {renderWidgets( - { - ...props, - layout: endChildren, - }, - startChildren?.length + centerChildren?.length, - )} - , - ]; -}