diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Iterate/Array/Examples.tsx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Iterate/Array/Examples.tsx index 6f8446992a6..a00cea2b265 100644 --- a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Iterate/Array/Examples.tsx +++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Iterate/Array/Examples.tsx @@ -6,6 +6,7 @@ import { Value, Form, Tools, + Wizard, } from '@dnb/eufemia/src/extensions/forms' export { Default as AnimatedContainer } from '../AnimatedContainer/Examples' @@ -368,7 +369,62 @@ export const WithVisibility = () => { ) } -export const InitialOpen = () => { +export const InitiallyOpen = () => { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) +} + +export const InitialOpenWithToolbarVariant = () => { return ( {() => { diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Iterate/Array/demos.mdx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Iterate/Array/demos.mdx index cf0f461a075..7b8e20894be 100644 --- a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Iterate/Array/demos.mdx +++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Iterate/Array/demos.mdx @@ -51,11 +51,15 @@ With an optional `title` and [Iterate.Toolbar](/uilib/extensions/forms/Iterate/T ### Initially open + + +### Minium one item + This example uses the container's `toolbarVariant` property with the value `minimumOneItem`. It hides the toolbar in the `EditContainer` when there is only one item in the array. And it hides the remove button in the `ViewContainer` when there is only one item in the array. - + ### With DataContext and add/remove buttons diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Iterate/PushContainer/Examples.tsx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Iterate/PushContainer/Examples.tsx index b61a1910194..1672f72837b 100644 --- a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Iterate/PushContainer/Examples.tsx +++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Iterate/PushContainer/Examples.tsx @@ -2,10 +2,12 @@ import { Field, Form, Iterate, + Tools, Value, } from '@dnb/eufemia/src/extensions/forms' import ComponentBox from '../../../../../../shared/tags/ComponentBox' import { Card, Flex } from '@dnb/eufemia/src' +import React from 'react' export { ViewAndEditContainer } from '../Array/Examples' @@ -97,3 +99,167 @@ export const InitiallyOpen = () => { ) } + +export const IsolatedData = () => { + return ( + + {() => { + const formData = { + persons: [ + { + firstName: 'Ola', + lastName: 'Nordmann', + }, + { + firstName: 'Kari', + lastName: 'Nordmann', + }, + { + firstName: 'Per', + lastName: 'Hansen', + }, + ], + } + + function RepresentativesView() { + return ( + + + + + + + ) + } + + function RepresentativesEdit() { + return ( + + + + + ) + } + + function ExistingPersonDetails() { + const { data, getValue } = Form.useData() + const person = getValue(data.selectedPerson)?.data || {} + + return ( + + + + + ) + } + + function NewPersonDetails() { + return ( + + + + + ) + } + + function PushContainerContent() { + const { data, update } = Form.useData() + + // Clear the PushContainer data when the selected person is "other", + // so the fields do not inherit existing data. + React.useLayoutEffect(() => { + if (data.selectedPerson === 'other') { + update('/pushContainerItems/0', {}) + } + }, [data.selectedPerson, update]) + + return ( + + + + + + typeof value === 'string' && value !== 'other', + }} + > + + + + value === 'other', + }} + > + + + + ) + } + + function RepresentativesCreateNew() { + return ( + { + return { + title: [data.firstName, data.lastName].join(' '), + value: '/persons/' + i, + data, + } + }), + }} + openButton={ + + } + showOpenButtonWhen={(list) => list.length > 0} + > + + + ) + } + + return ( + + Representatives + + + + + + + + + + + Data Context + + + + + ) + }} + + ) +} diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Iterate/PushContainer/demos.mdx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Iterate/PushContainer/demos.mdx index 15771354746..7a199f4c691 100644 --- a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Iterate/PushContainer/demos.mdx +++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Iterate/PushContainer/demos.mdx @@ -14,3 +14,9 @@ import * as Examples from './Examples' ### With existing data + +### Isolated data + +This demo shows how to use the `isolatedData` property to provide data to the PushContainer. + + diff --git a/packages/dnb-eufemia/src/extensions/forms/Iterate/Array/Array.tsx b/packages/dnb-eufemia/src/extensions/forms/Iterate/Array/Array.tsx index fef7b435027..93706831626 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Iterate/Array/Array.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/Iterate/Array/Array.tsx @@ -20,6 +20,7 @@ import { } from '../../../../components/flex/Container' import IterateItemContext, { IterateItemContextState, + ModeOptions, } from '../IterateItemContext' import SummaryListContext from '../../Value/SummaryList/SummaryListContext' import ValueBlockContext from '../../ValueBlock/ValueBlockContext' @@ -117,7 +118,7 @@ function ArrayComponent(props: Props) { { current: ContainerMode previous?: ContainerMode - options?: { omitFocusManagement?: boolean } + options?: ModeOptions } > >({}) @@ -180,7 +181,9 @@ function ArrayComponent(props: Props) { modesRef.current[id].current = mode modesRef.current[id].options = options delete isNewRef.current?.[id] - forceUpdate() + if (options?.preventUpdate !== true) { + forceUpdate() + } }, handleChange: (path, value) => { const newArrayValue = structuredClone(arrayValue) diff --git a/packages/dnb-eufemia/src/extensions/forms/Iterate/Array/ArrayItemArea.tsx b/packages/dnb-eufemia/src/extensions/forms/Iterate/Array/ArrayItemArea.tsx index 3c590b0fa8b..8a1ddce7f15 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Iterate/Array/ArrayItemArea.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/Iterate/Array/ArrayItemArea.tsx @@ -1,10 +1,4 @@ -import React, { - useCallback, - useContext, - useEffect, - useReducer, - useRef, -} from 'react' +import React, { useCallback, useContext, useReducer, useRef } from 'react' import classnames from 'classnames' import { Flex, HeightAnimation } from '../../../../components' import IterateItemContext, { @@ -15,6 +9,10 @@ import FieldBoundaryContext from '../../DataContext/FieldBoundary/FieldBoundaryC import { Props as FlexContainerProps } from '../../../../components/flex/Container' import { ContainerMode } from './types' +// SSR warning fix: https://gist.github.com/gaearon/e7d97cdf38a2907924ea12e4ebdf3c85 +const useLayoutEffect = + typeof window === 'undefined' ? React.useEffect : React.useLayoutEffect + export type ArrayItemAreaProps = { /** * Defines the variant of the ViewContainer, EditContainer or PushContainer. Can be `outline` or `basic`. @@ -50,32 +48,46 @@ function ArrayItemArea(props: Props & FlexContainerProps) { useContext(FieldBoundaryContext) || {} localContextRef.current = useContext(IterateItemContext) || {} const nextFocusElementRef = useRef() - const { isNew, value } = localContextRef.current - if (hasSubmitError || !value) { + const { isNew } = localContextRef.current + + const determineMode = useCallback(() => { + const { value, initialContainerMode } = localContextRef.current + if (initialContainerMode === 'auto') { + // - Set the container mode to "edit" if we have an error + if ( + hasSubmitError || + hasError || + !value || + (typeof value === 'object' && Object.keys(value).length === 0) + ) { + return 'edit' + } + } + }, [hasError, hasSubmitError]) + + if (determineMode() === 'edit') { localContextRef.current.containerMode = 'edit' + if (!localContextRef.current.modeOptions) { + localContextRef.current.modeOptions = {} + } + localContextRef.current.modeOptions.omitFocusManagement = true } if (localContextRef.current.containerMode === 'auto') { localContextRef.current.containerMode = 'view' } - const determineMode = useCallback(() => { - const { initialContainerMode, switchContainerMode } = - localContextRef.current - if ( - mode === 'edit' && - !hasSubmitError && - initialContainerMode === 'auto' - ) { - // - Set the container mode to "edit" if we have an error - if (hasError && !isNew) { - switchContainerMode('edit', { omitFocusManagement: true }) + useLayoutEffect(() => { + if (mode === 'edit') { + const editMode = determineMode() + if (editMode) { + const { switchContainerMode } = localContextRef.current + switchContainerMode?.(editMode, { + omitFocusManagement: true, + preventUpdate: true, + }) } } - }, [hasError, hasSubmitError, isNew, mode]) - - useEffect(() => { - determineMode() - }, [determineMode]) + }, [determineMode, mode]) const { handleRemove, index, previousContainerMode, containerMode } = localContextRef.current @@ -88,7 +100,7 @@ function ArrayItemArea(props: Props & FlexContainerProps) { forceUpdate() }, []) - useEffect(() => { + useLayoutEffect(() => { if (!isRemoving.current) { // - Set the open state, if it's controlled if (typeof open !== 'undefined') { diff --git a/packages/dnb-eufemia/src/extensions/forms/Iterate/Array/__tests__/ArrayItemArea.test.tsx b/packages/dnb-eufemia/src/extensions/forms/Iterate/Array/__tests__/ArrayItemArea.test.tsx index fd57c5fd703..8aabb337542 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Iterate/Array/__tests__/ArrayItemArea.test.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/Iterate/Array/__tests__/ArrayItemArea.test.tsx @@ -331,7 +331,7 @@ describe('ArrayItemArea', () => { expect(element).toHaveClass('dnb-height-animation--hidden') }) - it('opens in edit mode when falsy value was given', async () => { + it('opens in view mode by default when mode is view', () => { let containerMode = null const ContextConsumer = () => { @@ -343,7 +343,10 @@ describe('ArrayItemArea', () => { render( @@ -351,10 +354,131 @@ describe('ArrayItemArea', () => { ) + expect(containerMode).toBe('view') + }) + + it('opens in view mode by default when mode is edit', () => { + let containerMode = null + + const ContextConsumer = () => { + const context = React.useContext(IterateItemContext) + containerMode = context.containerMode + + return null + } + + render( + + + + + + ) + + expect(containerMode).toBe('view') + }) + + it('opens in edit mode when falsy value was given and mode is view', () => { + let containerMode = null + const switchContainerMode = jest.fn() + + const ContextConsumer = () => { + const context = React.useContext(IterateItemContext) + containerMode = context.containerMode + + return null + } + + render( + + + + + + ) + + expect(containerMode).toBe('edit') + expect(switchContainerMode).toHaveBeenCalledTimes(0) + }) + + it('opens in edit mode when falsy value was given and mode is edit', () => { + let containerMode = null + const switchContainerMode = jest.fn() + + const ContextConsumer = () => { + const context = React.useContext(IterateItemContext) + containerMode = context.containerMode + + return null + } + + render( + + + + + + ) + expect(containerMode).toBe('edit') + expect(switchContainerMode).toHaveBeenCalledTimes(1) + expect(switchContainerMode).toHaveBeenLastCalledWith('edit', { + omitFocusManagement: true, + preventUpdate: true, + }) + }) + + it('should thread empty object as falsy', () => { + let containerMode = null + const switchContainerMode = jest.fn() + + const ContextConsumer = () => { + const context = React.useContext(IterateItemContext) + containerMode = context.containerMode + + return null + } + + render( + + + + + + ) + + expect(containerMode).toBe('edit') + expect(switchContainerMode).toHaveBeenCalledTimes(1) + expect(switchContainerMode).toHaveBeenLastCalledWith('edit', { + omitFocusManagement: true, + preventUpdate: true, + }) }) - it('should call switchContainerMode when mode is edit and error is present', async () => { + it('should call switchContainerMode when mode is edit and error is present', () => { const switchContainerMode = jest.fn() render( @@ -375,6 +499,7 @@ describe('ArrayItemArea', () => { expect(switchContainerMode).toHaveBeenCalledTimes(1) expect(switchContainerMode).toHaveBeenLastCalledWith('edit', { omitFocusManagement: true, + preventUpdate: true, }) }) diff --git a/packages/dnb-eufemia/src/extensions/forms/Iterate/EditContainer/__tests__/EditAndViewContainer.test.tsx b/packages/dnb-eufemia/src/extensions/forms/Iterate/EditContainer/__tests__/EditAndViewContainer.test.tsx index 8a88a5640aa..db4e2e32dc1 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Iterate/EditContainer/__tests__/EditAndViewContainer.test.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/Iterate/EditContainer/__tests__/EditAndViewContainer.test.tsx @@ -208,7 +208,7 @@ describe('EditContainer and ViewContainer', () => { } render( - + View Content diff --git a/packages/dnb-eufemia/src/extensions/forms/Iterate/IterateItemContext.ts b/packages/dnb-eufemia/src/extensions/forms/Iterate/IterateItemContext.ts index 70d2238e103..8335eb74c80 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Iterate/IterateItemContext.ts +++ b/packages/dnb-eufemia/src/extensions/forms/Iterate/IterateItemContext.ts @@ -4,6 +4,7 @@ import { ContainerMode } from './Array/types' export type ModeOptions = { omitFocusManagement?: boolean + preventUpdate?: boolean } export interface IterateItemContextState { diff --git a/packages/dnb-eufemia/src/extensions/forms/Iterate/stories/Iterate.stories.tsx b/packages/dnb-eufemia/src/extensions/forms/Iterate/stories/Iterate.stories.tsx index 853ce68bd75..3e17807f36e 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Iterate/stories/Iterate.stories.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/Iterate/stories/Iterate.stories.tsx @@ -1,6 +1,6 @@ import React, { useCallback } from 'react' import { Field, Form, Iterate, Tools, Value, Wizard } from '../..' -import { Card, Flex, Section } from '../../../../components' +import { Card, Flex } from '../../../../components' export default { title: 'Eufemia/Extensions/Forms/Iterate', @@ -211,7 +211,7 @@ export const InitialOpen = () => { - + {count}