diff --git a/packages/main/src/components/Form/Form.cy.tsx b/packages/main/src/components/Form/Form.cy.tsx index 1e33cd2c845..1b478cb244f 100644 --- a/packages/main/src/components/Form/Form.cy.tsx +++ b/packages/main/src/components/Form/Form.cy.tsx @@ -1,3 +1,4 @@ +import { useReducer } from 'react'; import { createPortal } from 'react-dom'; import { InputType } from '../../enums/index.js'; import { Input, Label } from '../../webComponents/index.js'; @@ -30,6 +31,47 @@ const component = ( ); +const ConditionRenderingExample = () => { + const [show, toggle] = useReducer((prev) => !prev, false); + const [show2, toggle2] = useReducer((prev) => !prev, false); + const [show3, toggle3] = useReducer((prev) => !prev, false); + return ( + <> + + + +
+ + + + {show3 && } + {show && ( + + + + )} + {show2 && ( + + + + + + + + + )} + + + + + + + + + + ); +}; + describe('Form', () => { it('size S - labels and fields should cover full width', () => { cy.viewport(393, 852); // iPhone 14 Pro @@ -99,6 +141,53 @@ describe('Form', () => { cy.findByTestId('notSupported').should('not.exist'); }); + it('conditionally render FormItems & FormGroups', () => { + cy.mount(); + cy.findByText('Item 2').should('not.exist'); + + cy.findByText('Toggle Input').click(); + cy.findByText('Item 2').should('exist'); + cy.findByTestId('2').should('be.visible').as('item2'); + cy.get('@item2').parent().should('have.css', 'grid-column-start', '17').and('have.css', 'grid-row-start', '1'); + + cy.findByText('Toggle Group').click(); + cy.findByText('Group 1') + .should('be.visible') + .and('have.css', 'grid-column-start', '1') + .and('have.css', 'grid-row-start', '2'); + cy.findByTestId('g2').should('be.visible').as('g2'); + cy.get('@g2').parent().should('have.css', 'grid-column-start', '5').and('have.css', 'grid-row-start', '4'); + cy.findByTestId('2').should('be.visible').as('item2'); + cy.get('@item2').parent().should('have.css', 'grid-column-start', '17').and('have.css', 'grid-row-start', '1'); + + cy.findByText('Toggle Group2').click(); + cy.findByText('Empty Group') + .should('be.visible') + .and('have.css', 'grid-column-start', '13') + .and('have.css', 'grid-row-start', '1'); + cy.findByText('Group 1') + .should('be.visible') + .and('have.css', 'grid-column-start', '13') + .and('have.css', 'grid-row-start', '3'); + cy.findByTestId('g2').should('be.visible').as('g2'); + cy.get('@g2').parent().should('have.css', 'grid-column-start', '17').and('have.css', 'grid-row-start', '5'); + cy.findByTestId('2').should('be.visible').as('item2'); + cy.get('@item2').parent().should('have.css', 'grid-column-start', '5').and('have.css', 'grid-row-start', '4'); + + cy.findByText('Toggle Input').click(); + cy.findByText('Empty Group') + .should('be.visible') + .and('have.css', 'grid-column-start', '13') + .and('have.css', 'grid-row-start', '1'); + cy.findByText('Group 1') + .should('be.visible') + .and('have.css', 'grid-column-start', '1') + .and('have.css', 'grid-row-start', '3'); + cy.findByTestId('g2').should('be.visible').as('g2'); + cy.get('@g2').parent().should('have.css', 'grid-column-start', '5').and('have.css', 'grid-row-start', '5'); + cy.findByTestId('2').should('not.exist'); + }); + cypressPassThroughTestsFactory(Form, { children: ( diff --git a/packages/main/src/components/Form/FormContext.ts b/packages/main/src/components/Form/FormContext.ts index 76cc5db2883..3c1a5e36bed 100644 --- a/packages/main/src/components/Form/FormContext.ts +++ b/packages/main/src/components/Form/FormContext.ts @@ -1,7 +1,7 @@ import { createContext, useContext } from 'react'; import type { FormContextType, GroupContextType } from './types.js'; -export const FormContext = createContext({ labelSpan: null }); +export const FormContext = createContext({ labelSpan: null, recalcTrigger: 0 }); export function useFormContext() { return useContext(FormContext); diff --git a/packages/main/src/components/Form/index.tsx b/packages/main/src/components/Form/index.tsx index fb7634f9709..0ca4a0aa931 100644 --- a/packages/main/src/components/Form/index.tsx +++ b/packages/main/src/components/Form/index.tsx @@ -3,7 +3,7 @@ import { Device, useSyncRef } from '@ui5/webcomponents-react-base'; import { clsx } from 'clsx'; import type { ElementType, ReactNode } from 'react'; -import React, { forwardRef, useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import React, { forwardRef, useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react'; import { createUseStyles } from 'react-jss'; import { FormBackgroundDesign, TitleLevel } from '../../enums/index.js'; import type { CommonProps } from '../../interfaces/index.js'; @@ -12,6 +12,10 @@ import { styles } from './Form.jss.js'; import { FormContext } from './FormContext.js'; import type { FormContextType, FormElementTypes, FormGroupLayoutInfo, FormItemLayoutInfo, ItemInfo } from './types.js'; +const recalcReducerFn = (prev: number) => { + return prev + 1; +}; + const useStyles = createUseStyles(styles, { name: 'Form' }); export interface FormPropTypes extends CommonProps { @@ -165,8 +169,8 @@ const Form = forwardRef((props, ref) => { const currentNumberOfColumns = columnsMap.get(currentRange); const registerItem = useCallback((id: string, type: FormElementTypes, groupId?: string) => { - setItems((state) => { - const clonedMap = new Map(state); + setItems((prev) => { + const clonedMap = new Map(prev); if (groupId) { const groupItem = clonedMap.get(groupId); if (groupItem) { @@ -201,7 +205,7 @@ const Form = forwardRef((props, ref) => { }); }, []); - const formLayoutContextValue = useMemo((): Omit => { + const formLayoutContextValue = useMemo((): Omit => { const formItems: FormItemLayoutInfo[] = []; const formGroups: FormGroupLayoutInfo[] = []; @@ -261,8 +265,35 @@ const Form = forwardRef((props, ref) => { const formClassNames = clsx(classes.form, classes[backgroundDesign.toLowerCase()]); const CustomTag = as as ElementType; + const prevFormItems = useRef(undefined); + const prevFormGroups = useRef(undefined); + + const [recalcTrigger, fireRecalc] = useReducer(recalcReducerFn, 0, undefined); + useEffect(() => { + if (prevFormItems.current || prevFormGroups.current) { + let hasChanged = + formLayoutContextValue.formItems.length !== prevFormItems.current.length || + formLayoutContextValue.formGroups.length !== prevFormGroups.current.length; + if (!hasChanged) { + hasChanged = !formLayoutContextValue.formGroups.every( + (item, index) => prevFormGroups.current.findIndex((element) => element.id === item.id) === index + ); + } + if (!hasChanged) { + hasChanged = !formLayoutContextValue.formItems.every( + (item, index) => prevFormItems.current.findIndex((element) => element.id === item.id) === index + ); + } + if (hasChanged) { + fireRecalc(); + } + } + prevFormItems.current = formLayoutContextValue.formItems; + prevFormGroups.current = formLayoutContextValue.formGroups; + }, [formLayoutContextValue.formItems, formLayoutContextValue.formGroups]); + return ( - + void; labelSpan: null | number; rowsWithGroup?: Record; + recalcTrigger: number; }; export type GroupContextType = { diff --git a/packages/main/src/components/FormGroup/index.tsx b/packages/main/src/components/FormGroup/index.tsx index 0ffaeebd375..cb8e0b207a3 100644 --- a/packages/main/src/components/FormGroup/index.tsx +++ b/packages/main/src/components/FormGroup/index.tsx @@ -26,15 +26,18 @@ export interface FormGroupPropTypes { */ const FormGroup = (props: FormGroupPropTypes) => { const { titleText, children } = props; - const { formGroups: layoutInfos, registerItem, unregisterItem, labelSpan } = useFormContext(); + const { formGroups: layoutInfos, registerItem, unregisterItem, labelSpan, recalcTrigger } = useFormContext(); const uniqueId = useIsomorphicId(); useEffect(() => { registerItem?.(uniqueId, 'formGroup'); return () => unregisterItem?.(uniqueId); - }, [uniqueId, registerItem, unregisterItem]); + }, [uniqueId, registerItem, unregisterItem, recalcTrigger]); - const layoutInfo = useMemo(() => layoutInfos?.find(({ id: groupId }) => uniqueId === groupId), [layoutInfos]); + const layoutInfo = useMemo( + () => layoutInfos?.find(({ id: groupId }) => uniqueId === groupId), + [layoutInfos, uniqueId] + ); if (!layoutInfo) return null; const { columnIndex, rowIndex } = layoutInfo; diff --git a/packages/main/src/components/FormItem/index.tsx b/packages/main/src/components/FormItem/index.tsx index 6d156c45c02..573e0b61bfc 100644 --- a/packages/main/src/components/FormItem/index.tsx +++ b/packages/main/src/components/FormItem/index.tsx @@ -124,9 +124,16 @@ const getContentForHtmlLabel = (label: ReactNode) => { * __Note__: The `FormItem` is only used for calculating the final layout of the `Form`, thus it doesn't accept any other props than `label` and `children`, especially no `className`, `style` or `ref`. */ const FormItem = (props: FormItemPropTypes) => { - const { label, children } = props as InternalProps; const uniqueId = useIsomorphicId(); - const { formItems: layoutInfos, registerItem, unregisterItem, labelSpan, rowsWithGroup } = useFormContext(); + const { label, children } = props as InternalProps; + const { + formItems: layoutInfos, + registerItem, + unregisterItem, + labelSpan, + rowsWithGroup, + recalcTrigger + } = useFormContext(); const groupContext = useFormGroupContext(); const classes = useStyles(); @@ -135,7 +142,7 @@ const FormItem = (props: FormItemPropTypes) => { return () => { unregisterItem?.(uniqueId, groupContext.id); }; - }, [uniqueId, registerItem, unregisterItem, groupContext.id]); + }, [uniqueId, registerItem, unregisterItem, groupContext.id, recalcTrigger]); const layoutInfo = useMemo(() => layoutInfos?.find(({ id: itemId }) => uniqueId === itemId), [layoutInfos, uniqueId]);