Skip to content

Commit

Permalink
feat(Forms): add validateFieldsInitially to Form.Section
Browse files Browse the repository at this point in the history
  • Loading branch information
tujoworker committed Aug 28, 2024
1 parent 416df35 commit d543368
Show file tree
Hide file tree
Showing 9 changed files with 138 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,33 @@ import FieldBoundaryContext, {
import DataContext from '../Context'
import { Path } from '../../types'

export default function FieldBoundaryProvider({ children }) {
export type Props = {
showErrors?: boolean
onPathError?: (path: Path, error: Error) => void
children: React.ReactNode
}

export default function FieldBoundaryProvider(props: Props) {
const { showErrors = false, onPathError = null, children } = props
const [, forceUpdate] = useReducer(() => ({}), {})
const { showAllErrors, hasVisibleError } = useContext(DataContext)

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

const setFieldError = useCallback((path: Path, error: Error) => {
if (error) {
errorsRef.current[path] = !!error
} else {
delete errorsRef.current?.[path]
}
}, [])
const setFieldError = useCallback(
(path: Path, error: Error) => {
onPathError?.(path, error)
if (error) {
errorsRef.current[path] = !!error
} else {
delete errorsRef.current?.[path]
}
},
[onPathError]
)

const setShowBoundaryErrors = useCallback(
(showBoundaryErrors: boolean) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,27 @@ describe('FieldBoundaryProvider', () => {
})
})

it('should set showBoundaryErrorsRef to true when showErrors is true', async () => {
const contextRef: React.MutableRefObject<FieldBoundaryContextState> =
React.createRef()

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

render(
<Provider>
<FieldBoundaryProvider showErrors>
<ContextConsumer />
<Field.String />
</FieldBoundaryProvider>
</Provider>
)

expect(contextRef.current.showBoundaryErrors).toBe(true)
})

it('should set error in boundary context', async () => {
const contextRef: React.MutableRefObject<FieldBoundaryContextState> =
React.createRef()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useMemo } from 'react'
import React, { useCallback, useContext, useMemo } from 'react'
import classnames from 'classnames'
import { convertJsxToString } from '../../../../../shared/component-helper'
import { Flex } from '../../../../../components'
Expand All @@ -10,6 +10,9 @@ import SectionContainer, {
SectionContainerProps,
} from '../containers/SectionContainer'
import Toolbar from '../containers/Toolbar'
import SectionContext from '../SectionContext'
import { Path } from '../../../types'
import SectionContainerContext from '../containers/SectionContainerContext'

export type Props = {
title?: React.ReactNode
Expand All @@ -20,9 +23,24 @@ export type AllProps = Props & SectionContainerProps & FlexContainerProps
function EditContainer(props: AllProps) {
const { children, className, title, ...restProps } = props || {}
const ariaLabel = useMemo(() => convertJsxToString(title), [title])
const { switchContainerMode } = useContext(SectionContainerContext) || {}
const sectionContext = useContext(SectionContext)
const { validateFieldsInitially } = sectionContext?.props || {}

const onPathError = useCallback(
(path: Path, error: Error) => {
if (validateFieldsInitially && error instanceof Error) {
switchContainerMode?.('edit')
}
},
[switchContainerMode, validateFieldsInitially]
)

return (
<FieldBoundaryProvider>
<FieldBoundaryProvider
showErrors={validateFieldsInitially}
onPathError={onPathError}
>
<SectionContainer
mode="edit"
ariaLabel={ariaLabel}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,49 @@ describe('EditContainer and ViewContainer', () => {
expect(input).toHaveValue('bar')
})

it('when "validateFieldsInitially" is set to true, fields should show their errors and the mode should be "edit"', async () => {
let containerMode = null

const ContextConsumer = () => {
const context = React.useContext(SectionContainerContext)
containerMode = context.containerMode

return null
}

render(
<Form.Handler>
<Form.Section validateFieldsInitially path="/">
<EditContainer>
<Field.String path="/foo" required />
</EditContainer>
<ViewContainer>content</ViewContainer>
<ContextConsumer />
</Form.Section>
</Form.Handler>
)

expect(containerMode).toBe('edit')
expect(document.querySelector('.dnb-form-status')).toBeInTheDocument()
expect(document.querySelector('.dnb-form-status')).toHaveTextContent(
nb.Field.errorRequired
)

const input = document.querySelector('input')
expect(input).toHaveValue('')

await userEvent.type(input, 'something')

expect(
document.querySelector('.dnb-form-status')
).not.toBeInTheDocument()

const [doneButton] = Array.from(document.querySelectorAll('button'))
await userEvent.click(doneButton)

expect(containerMode).toBe('view')
})

it('should reset entered data on Cancel press when path is set', async () => {
let containerMode = null

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,13 @@ export type SectionProps<overwriteProps = OverwritePropsDefaults> = {
*/
containerMode?: ContainerMode

/**
* When set to `true`, the mode will initially be "edit" if fields contain errors.
* Fields will automatically get `validateInitially` and show their error messages.
* Defaults to `false`.
*/
validateFieldsInitially?: boolean

/**
* Only for internal use and undocumented for now.
* Prioritize error techniques for the section.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ export const SectionProperties: PropertiesTableProps = {
type: 'string',
status: 'optional',
},
validateFieldsInitially: {
doc: 'When set to `true`, the mode will initially be "edit" if fields contain errors. Fields will automatically get `validateInitially` and show their error messages. Defaults to `false`.',
type: 'boolean',
status: 'optional',
},
children: {
doc: 'All the fields and values inside the section.',
type: 'React.Node',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,11 +102,21 @@ function SectionContainer(props: Props & FlexContainerProps) {
[onAnimationEnd, switchContainerMode]
)

const preventAnimationRef = useRef(true)
useEffect(() => {
setTimeout(() => {
preventAnimationRef.current = false
forceUpdate()
}, 1000) // Initially, we don't want to animate
}, [])

return (
<HeightAnimation
className={classnames(
'dnb-forms-section-block',
variant && `dnb-forms-section-block--variant-${variant}`,
preventAnimationRef.current &&
'dnb-forms-section-block--no-animation',
contextRef.current.hasSubmitError &&
'dnb-forms-section-block--error',
className
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@
}
}

&--no-animation &__inner {
transform: translateY(0);
}

&.dnb-height-animation--is-visible &__inner {
transform: translateY(-0.5rem);
}
Expand Down
10 changes: 8 additions & 2 deletions packages/dnb-eufemia/src/extensions/forms/hooks/useFieldProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1214,10 +1214,16 @@ export default function useFieldProps<Value, EmptyValue, Props>(
// something else that should lead to showing the user all errors.
revealError()
}
} else if (showBoundaryErrors === false) {
} else if (showBoundaryErrors === false && !validateInitially) {
hideError()
}
}, [hideError, revealError, showAllErrors, showBoundaryErrors])
}, [
hideError,
revealError,
showAllErrors,
showBoundaryErrors,
validateInitially,
])

useEffect(() => {
if (
Expand Down

0 comments on commit d543368

Please sign in to comment.