Skip to content

Commit

Permalink
fix(forms): validate fields inside Iterate.EditContainer when done …
Browse files Browse the repository at this point in the history
…button is pressed (#3850)
  • Loading branch information
tujoworker authored Aug 21, 2024
1 parent 0d50272 commit 2a2272e
Show file tree
Hide file tree
Showing 8 changed files with 186 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,16 @@ export interface FieldBoundaryContextState {
*/
errorsRef?: React.RefObject<unknown>

/**
* 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.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import React, { useCallback, useContext, useRef } from 'react'
import React, { useCallback, useContext, useReducer, useRef } from 'react'
import FieldBoundaryContext, {
FieldBoundaryContextState,
} from './FieldBoundaryContext'
import DataContext from '../Context'
import { Path } from '../../types'

export default function FieldBoundaryProvider({ children }) {
const [, forceUpdate] = useReducer(() => ({}), {})
const { showAllErrors, hasVisibleError } = useContext(DataContext)

const errorsRef = useRef<Record<Path, boolean>>({})
const showBoundaryErrorsRef = useRef<boolean>(false)
const hasError = Object.keys(errorsRef.current || {}).length > 0
const hasSubmitError = showAllErrors && hasError

Expand All @@ -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,
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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,
})
Expand Down Expand Up @@ -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<FieldBoundaryContextState> =
React.createRef()

const ContextConsumer = () => {
contextRef.current = useContext(FieldBoundaryContext)
return null
}

const { rerender } = render(
<Provider>
<FieldBoundaryProvider>
<ContextConsumer />
<Field.String />
</FieldBoundaryProvider>
</Provider>
)

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(
<Provider>
<FieldBoundaryProvider>
<ContextConsumer />
<Field.String required />
</FieldBoundaryProvider>
</Provider>
)

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<FieldBoundaryContextState> =
React.createRef()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) || {}

Expand All @@ -48,20 +49,31 @@ 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?.()
} else {
switchContainerMode?.('view')
}
}
}, [commitHandleRef, hasVisibleError, switchContainerMode])
}, [
commitHandleRef,
hasError,
hasVisibleError,
setShowBoundaryErrors,
switchContainerMode,
])

return (
<>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<Form.Handler>
<Iterate.Array value={['']} onChange={onChange}>
<EditContainer>
<Field.String required itemPath="/" />
</EditContainer>
<ViewContainer>content</ViewContainer>
</Iterate.Array>
</Form.Handler>
)

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()
})
})
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -184,6 +185,38 @@ describe('EditContainer', () => {
).toHaveAttribute('data-attr', 'value')
})

it('should validate on done button click', async () => {
render(
<Form.Handler>
<Iterate.Array value={['']}>
<EditContainer>
<Field.String required itemPath="/" />
</EditContainer>
</Iterate.Array>
</Form.Handler>
)

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(
Expand Down Expand Up @@ -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'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,6 @@ const CreateNewEntry = () => {
}

const MyViewItem = () => {
const item = Iterate.useItem()
console.log('index:', item.index)

return (
<Iterate.ViewContainer title="Account holder {itemNr}">
<Value.SummaryList>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ export default function useFieldProps<Value, EmptyValue, Props>(
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({
Expand Down Expand Up @@ -1207,16 +1207,17 @@ export default function useFieldProps<Value, EmptyValue, Props>(
])

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 (
Expand Down

0 comments on commit 2a2272e

Please sign in to comment.