diff --git a/packages/dnb-eufemia/src/extensions/forms/DataContext/FieldBoundary/FieldBoundaryContext.ts b/packages/dnb-eufemia/src/extensions/forms/DataContext/FieldBoundary/FieldBoundaryContext.ts index 4643baa7c11..05964b7fd7a 100644 --- a/packages/dnb-eufemia/src/extensions/forms/DataContext/FieldBoundary/FieldBoundaryContext.ts +++ b/packages/dnb-eufemia/src/extensions/forms/DataContext/FieldBoundary/FieldBoundaryContext.ts @@ -22,6 +22,16 @@ export interface FieldBoundaryContextState { */ errorsRef?: React.RefObject + /** + * Will be set to true when the boundary context error state should be shown. + */ + showBoundaryErrors?: boolean + + /** + * To set the boundary context error state. + */ + setShowBoundaryErrors?: (showBoundaryErrors: boolean) => void + /** * To set the local error state. */ diff --git a/packages/dnb-eufemia/src/extensions/forms/DataContext/FieldBoundary/FieldBoundaryProvider.tsx b/packages/dnb-eufemia/src/extensions/forms/DataContext/FieldBoundary/FieldBoundaryProvider.tsx index 7235a12114e..803b8a47dea 100644 --- a/packages/dnb-eufemia/src/extensions/forms/DataContext/FieldBoundary/FieldBoundaryProvider.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/DataContext/FieldBoundary/FieldBoundaryProvider.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useContext, useRef } from 'react' +import React, { useCallback, useContext, useReducer, useRef } from 'react' import FieldBoundaryContext, { FieldBoundaryContextState, } from './FieldBoundaryContext' @@ -6,9 +6,11 @@ import DataContext from '../Context' import { Path } from '../../types' export default function FieldBoundaryProvider({ children }) { + const [, forceUpdate] = useReducer(() => ({}), {}) const { showAllErrors, hasVisibleError } = useContext(DataContext) const errorsRef = useRef>({}) + const showBoundaryErrorsRef = useRef(false) const hasError = Object.keys(errorsRef.current || {}).length > 0 const hasSubmitError = showAllErrors && hasError @@ -20,11 +22,21 @@ export default function FieldBoundaryProvider({ children }) { } }, []) + const setShowBoundaryErrors = useCallback( + (showBoundaryErrors: boolean) => { + showBoundaryErrorsRef.current = showBoundaryErrors + forceUpdate() + }, + [] + ) + const context: FieldBoundaryContextState = { hasError, hasSubmitError, hasVisibleError, errorsRef, + showBoundaryErrors: showBoundaryErrorsRef.current, + setShowBoundaryErrors, setFieldError, } diff --git a/packages/dnb-eufemia/src/extensions/forms/DataContext/FieldBoundary/__tests__/FieldBoundaryProvider.test.tsx b/packages/dnb-eufemia/src/extensions/forms/DataContext/FieldBoundary/__tests__/FieldBoundaryProvider.test.tsx index 0642bf1b4bd..7055e26218b 100644 --- a/packages/dnb-eufemia/src/extensions/forms/DataContext/FieldBoundary/__tests__/FieldBoundaryProvider.test.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/DataContext/FieldBoundary/__tests__/FieldBoundaryProvider.test.tsx @@ -1,5 +1,5 @@ import React, { useContext } from 'react' -import { render } from '@testing-library/react' +import { render, act } from '@testing-library/react' import FieldBoundaryProvider from '../FieldBoundaryProvider' import FieldBoundaryContext, { FieldBoundaryContextState, @@ -75,12 +75,15 @@ describe('FieldBoundaryProvider', () => { hasError: true, hasSubmitError: true, hasVisibleError: true, + showBoundaryErrors: false, setFieldError: expect.any(Function), + setShowBoundaryErrors: expect.any(Function), }) expect(contextRef.current.hasError).toBe(true) expect(contextRef.current.hasSubmitError).toBe(true) expect(contextRef.current.hasVisibleError).toBe(true) + expect(contextRef.current.showBoundaryErrors).toBe(false) expect(contextRef.current.errorsRef.current).toMatchObject({ 'id-r0': true, }) @@ -115,11 +118,63 @@ describe('FieldBoundaryProvider', () => { expect(contextRef.current.hasError).toBe(true) expect(contextRef.current.hasSubmitError).toBe(true) expect(contextRef.current.hasVisibleError).toBe(true) + expect(contextRef.current.showBoundaryErrors).toBe(false) expect(contextRef.current.errorsRef.current).toMatchObject({ '/bar': true, }) }) + it('should set error in boundary context', async () => { + const contextRef: React.MutableRefObject = + React.createRef() + + const ContextConsumer = () => { + contextRef.current = useContext(FieldBoundaryContext) + return null + } + + const { rerender } = render( + + + + + + + ) + + expect(contextRef.current.hasError).toBe(false) + expect(contextRef.current.hasSubmitError).toBe(false) + expect(contextRef.current.hasVisibleError).toBe(false) + expect(contextRef.current.showBoundaryErrors).toBe(false) + + rerender( + + + + + + + ) + + act(() => { + contextRef.current.setShowBoundaryErrors?.(true) + }) + + expect(contextRef.current.hasError).toBe(true) + expect(contextRef.current.hasSubmitError).toBe(false) + expect(contextRef.current.hasVisibleError).toBe(true) + expect(contextRef.current.showBoundaryErrors).toBe(true) + + act(() => { + contextRef.current.setShowBoundaryErrors?.(false) + }) + + expect(contextRef.current.hasError).toBe(true) + expect(contextRef.current.hasSubmitError).toBe(false) + expect(contextRef.current.hasVisibleError).toBe(false) + expect(contextRef.current.showBoundaryErrors).toBe(false) + }) + it('should set error in context with continuousValidation', async () => { const contextRef: React.MutableRefObject = React.createRef() diff --git a/packages/dnb-eufemia/src/extensions/forms/Iterate/EditContainer/EditToolbarTools.tsx b/packages/dnb-eufemia/src/extensions/forms/Iterate/EditContainer/EditToolbarTools.tsx index fed5c32e1d8..fb3486e954c 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Iterate/EditContainer/EditToolbarTools.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/Iterate/EditContainer/EditToolbarTools.tsx @@ -23,7 +23,8 @@ export default function EditToolbarTools() { index, isNew, } = useContext(IterateItemContext) || {} - const { hasVisibleError } = useContext(FieldBoundaryContext) || {} + const { hasError, hasVisibleError, setShowBoundaryErrors } = + useContext(FieldBoundaryContext) || {} const { entries, commitHandleRef } = useContext(PushContainerContext) || {} @@ -48,12 +49,17 @@ export default function EditToolbarTools() { restoreOriginalValue?.(valueBackupRef.current) } setShowError(false) + setShowBoundaryErrors?.(false) switchContainerMode?.('view') - }, [restoreOriginalValue, switchContainerMode]) + }, [restoreOriginalValue, setShowBoundaryErrors, switchContainerMode]) const doneHandler = useCallback(() => { - if (hasVisibleError) { - setShowError(true) + if (hasError) { + setShowBoundaryErrors?.(true) + if (hasVisibleError) { + setShowError(true) + } } else { + setShowBoundaryErrors?.(false) setShowError(false) if (commitHandleRef) { commitHandleRef.current?.() @@ -61,7 +67,13 @@ export default function EditToolbarTools() { switchContainerMode?.('view') } } - }, [commitHandleRef, hasVisibleError, switchContainerMode]) + }, [ + commitHandleRef, + hasError, + hasVisibleError, + setShowBoundaryErrors, + switchContainerMode, + ]) return ( <> 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 25793e85e2b..d509f08f7b0 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 @@ -235,4 +235,54 @@ describe('EditContainer and ViewContainer', () => { expect(viewBlock).toHaveClass('dnb-forms-section-block--variant-basic') expect(editBlock).toHaveClass('dnb-forms-section-block--variant-basic') }) + + it('should validate on done button click', async () => { + const onChange = jest.fn() + + render( + + + + + + content + + + ) + + expect( + document.querySelector('.dnb-form-status') + ).not.toBeInTheDocument() + + const [doneButton, cancelButton, editButton] = Array.from( + document.querySelectorAll('button') + ) + expect(doneButton).toHaveTextContent(nb.doneButton) + expect(cancelButton).toHaveTextContent(nb.cancelButton) + expect(editButton).toHaveTextContent( + nbNO['nb-NO'].IterateViewContainer.editButton + ) + await userEvent.click(doneButton) + + expect(document.querySelector('.dnb-form-status')).toBeInTheDocument() + expect(onChange).toHaveBeenCalledTimes(0) + + await userEvent.click(cancelButton) + await userEvent.click(editButton) + + expect( + document.querySelector('.dnb-form-status') + ).not.toBeInTheDocument() + + await userEvent.click(doneButton) + + expect(document.querySelector('.dnb-form-status')).toBeInTheDocument() + + await userEvent.type(document.querySelector('input'), 'foo') + await userEvent.click(doneButton) + + expect( + document.querySelector('.dnb-form-status') + ).not.toBeInTheDocument() + }) }) diff --git a/packages/dnb-eufemia/src/extensions/forms/Iterate/EditContainer/__tests__/EditContainer.test.tsx b/packages/dnb-eufemia/src/extensions/forms/Iterate/EditContainer/__tests__/EditContainer.test.tsx index 5d835d90405..31984fd95c1 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Iterate/EditContainer/__tests__/EditContainer.test.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/Iterate/EditContainer/__tests__/EditContainer.test.tsx @@ -1,6 +1,7 @@ import React from 'react' import { render, fireEvent, screen } from '@testing-library/react' -import { Form, Iterate } from '../../..' +import userEvent from '@testing-library/user-event' +import { Field, Form, Iterate } from '../../..' import IterateItemContext from '../../IterateItemContext' import EditContainer from '../EditContainer' import nbNO from '../../../constants/locales/nb-NO' @@ -184,6 +185,38 @@ describe('EditContainer', () => { ).toHaveAttribute('data-attr', 'value') }) + it('should validate on done button click', async () => { + render( + + + + + + + + ) + + expect( + document.querySelector('.dnb-form-status') + ).not.toBeInTheDocument() + + const [doneButton] = Array.from(document.querySelectorAll('button')) + + expect(doneButton).toHaveTextContent(nb.doneButton) + + await userEvent.click(doneButton) + + expect(document.querySelector('.dnb-form-status')).toBeInTheDocument() + + const input = document.querySelector('input') + await userEvent.type(input, 'foo') + await userEvent.click(doneButton) + + expect( + document.querySelector('.dnb-form-status') + ).not.toBeInTheDocument() + }) + describe('to have buttons with correct text', () => { it('and isNew is true', () => { render( @@ -249,6 +282,7 @@ describe('EditContainer', () => { expect(screen.getByText(remove)).toBeInTheDocument() expect(screen.getByText(done)).toBeInTheDocument() }) + it('supports translations cancelButton and doneButton from Form.Handler', () => { const done = 'custom-translation-done-button-text' const cancel = 'custom-translation-cancel-button-text' 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 692b0e6ca7d..c18f34716ea 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 @@ -90,9 +90,6 @@ const CreateNewEntry = () => { } const MyViewItem = () => { - const item = Iterate.useItem() - console.log('index:', item.index) - return ( diff --git a/packages/dnb-eufemia/src/extensions/forms/hooks/useFieldProps.ts b/packages/dnb-eufemia/src/extensions/forms/hooks/useFieldProps.ts index 1c5af6e4091..16eb95d41b9 100644 --- a/packages/dnb-eufemia/src/extensions/forms/hooks/useFieldProps.ts +++ b/packages/dnb-eufemia/src/extensions/forms/hooks/useFieldProps.ts @@ -168,7 +168,7 @@ export default function useFieldProps( const { handleChange: handleChangeIterateContext } = iterateItemContext ?? {} const { path: sectionPath, errorPrioritization } = sectionContext ?? {} - const { setFieldError } = fieldBoundaryContext ?? {} + const { setFieldError, showBoundaryErrors } = fieldBoundaryContext ?? {} const hasPath = Boolean(pathProp) const { path, identifier, makeIteratePath } = usePath({ @@ -1207,16 +1207,17 @@ export default function useFieldProps( ]) useEffect(() => { - if (showAllErrors) { + if (showAllErrors || showBoundaryErrors) { // In case of async validation, we don't want to show existing errors before the validation has been completed if (fieldStateRef.current !== 'validating') { // If showError on a surrounding data context was changed and set to true, it is because the user clicked next, submit or // something else that should lead to showing the user all errors. revealError() - forceUpdate() } + } else if (showBoundaryErrors === false) { + hideError() } - }, [showAllErrors, revealError]) + }, [hideError, revealError, showAllErrors, showBoundaryErrors]) useEffect(() => { if (