Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(Forms): add isValid to Form.Visibility for showing content based on the validation of a field #4038

Merged
merged 7 commits into from
Oct 8, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -310,3 +310,25 @@ export function InheritVisibility() {
</ComponentBox>
)
}

export function VisibilityOnValidation() {
return (
<ComponentBox>
<Form.Handler>
<Card stack>
<Field.Name.First path="/foo" required />

<Form.Visibility
visibleWhen={{
path: '/foo',
isValid: true,
}}
animate
>
<Value.Name.First path="/foo" />
</Form.Visibility>
</Card>
</Form.Handler>
</ComponentBox>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,7 @@ In this example we filter out all fields that have the `data-exclude-field` attr
### Inherit visibility

<Examples.InheritVisibility />

### Show children when field has no errors (validation)

<Examples.VisibilityOnValidation />
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,71 @@ showTabs: true

## Description

The `Form.Visibility` component makes it possible to show or hide components on the screen based on the state of data. It can either be fed with the values directly via properties, or it can read data from a surrounding [Form.Handler](/uilib/extensions/forms/Form/Handler) and show or hide components based on the data it points to.
The `Form.Visibility` component allows you to conditionally show or hide components based on the state of data or field validation. You can either provide the values directly via properties or let it read data from a surrounding [Form.Handler](/uilib/extensions/forms/Form/Handler). This enables dynamic visibility control based on the paths it points to.

### Data driven visibility

There are several [properties](/uilib/extensions/forms/Form/Visibility/properties/) you can use to control visibility, such as `pathDefined`, `pathTruthy`, `pathTrue` etc.

```tsx
import { Form } from '@dnb/eufemia/extensions/forms'
import { Form, Field } from '@dnb/eufemia/extensions/forms'

render(
<>
<Field.Boolean path="/myState" />
<Form.Visibility pathTrue="/myState">
show me when the state value is true
show me when the data value is true
</Form.Visibility>
</>,
)
```

#### Dynamic value driven visibility

You can also use the `visibleWhen` property to conditionally show the children based on the data value of the path.

```tsx
import { Form, Field } from '@dnb/eufemia/extensions/forms'

render(
<>
<Field.Boolean path="/myState" />
<Form.Visibility
visibleWhen={{
path: '/myState',
hasValue: (value) => value === true,
}}
>
show me when the data value is true
</Form.Visibility>
</>,
)
```

### Validation driven visibility

You can conditionally display children based on field validation by using the `visibleWhen` property with `isValid: true`:

```tsx
import { Form, Field } from '@dnb/eufemia/extensions/forms'

render(
<>
<Field.Boolean path="/myField" />
<Form.Visibility
visibleWhen={{
path: '/myField',
isValid: true,
}}
>
show me when the validation succeeds
</Form.Visibility>
</>,
)
```

To prevent visibility changes during user interactions like typing, it shows the children first when the field both has no errors and has lost focus (blurred). You can use the `continuousValidation: true` property to immediately show the children when the field has no errors.

## Accessibility

Children of the `Form.Visibility` component will be hidden from screen readers when visually hidden, even if `keepInDOM` is enabled. You don't need to do anything to make the content additionally inaccessible.
Expand All @@ -32,7 +83,7 @@ render(
<>
<Field.Boolean path="/myState" />
<Form.Visibility pathTrue="/myState" animate>
show me when the state value is true
show me when the data value is true
</Form.Visibility>
</>,
)
Expand All @@ -48,7 +99,7 @@ render(
<>
<Field.Boolean path="/myState" />
<Form.Visibility pathTrue="/myState" keepInDOM>
show me when the state value is true
show me when the data value is true
</Form.Visibility>
</>,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,21 @@ import AsyncChangeExample from './Form/Handler/parts/async-change-example.mdx'

**Table of Contents**

- [Quick start](#quick-start)
- [Getting started](#getting-started)
- [Creating forms](#creating-forms)
- [State management](#state-management)
- [What is a JSON Pointer?](#what-is-a-json-pointer)
- [Data handling](#data-handling)
- [Visible data](#visible-data)
- [Filter data](#filter-data)
- [Filter data during submit](#filter-data-during-submit)
- [Transforming data](#transforming-data)
- [Complex objects in the data context](#complex-objects-in-the-data-context)
- [Async form handling](#async-form-handling)
- [Field components](#field-components)
- [Value components](#value-components)
- [Inherit visibility from fields](#inherit-visibility-from-fields)
- [Conditionally display content](#conditionally-display-content)
- [Async form behavior](#async-form-behavior)
- [onChange and autosave](#onchange-and-autosave)
- [Async field validation](#async-field-validation)
Expand All @@ -37,7 +41,8 @@ import AsyncChangeExample from './Form/Handler/parts/async-change-example.mdx'
- [required](#required)
- [pattern](#pattern)
- [schema](#schema)
- [validator](#validator)
- [onBlurValidator and validator](#onblurvalidator-and-validator)
- [Connect with another field](#connect-with-another-field)
- [Async validation](#async-validation)
- [Async validator with debounce](#async-validator-with-debounce)
- [Localization and translation](#localization-and-translation)
Expand All @@ -46,7 +51,7 @@ import AsyncChangeExample from './Form/Handler/parts/async-change-example.mdx'
- [Use the shared Provider to customize translations](#use-the-shared-provider-to-customize-translations)
- [Layout](#layout)
- [Best practices](#best-practices)
- [Create your own component](#create-your-own-component)
- [Create your own component](#create-your-own-component)

<QuickStart />

Expand Down Expand Up @@ -319,6 +324,23 @@ User entered data will always be stored internally in the data context, even if

You can use the `inheritVisibility` property on the [Value.\*](/uilib/extensions/forms/Value/) components to inherit the visibility from the field with the same path.

### Conditionally display content

You can conditionally display content using the [Form.Visibility](/uilib/extensions/forms/Form/Visibility/) component. This allows you to show or hide its children based on the validation (A.) or the value (B.) of another path (data entry).

```tsx
<Form.Visibility
animate
visibleWhen={{
path: '/myField',
hasValue: (value) => value === 'foo', // A. Value based
isValid: true, // B. Validation based
}}
>
<Field.String path="/myField" />
</Form.Visibility>
```

### Async form behavior

This feature allows you to perform asynchronous operations such as fetching data from an API – without additional state management.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export type MountState = {
isPreMounted?: boolean
isMounted?: boolean
isVisible?: boolean
isFocused?: boolean
wasStepChange?: boolean
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,23 @@ import VisibilityContext from './VisibilityContext'
export type VisibleWhen =
| {
path: Path
hasValue: unknown
hasValue: unknown | ((value: unknown) => boolean)
}
| {
itemPath: Path
hasValue: unknown
hasValue: unknown | ((value: unknown) => boolean)
}
| {
path: Path
isValid: boolean
continuousValidation?: boolean
tujoworker marked this conversation as resolved.
Show resolved Hide resolved
}
| {
itemPath: Path
isValid: boolean
continuousValidation?: boolean
}

/**
* @deprecated Will be removed in v11!
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
import { PropertiesTableProps } from '../../../../shared/types'

export const VisibilityProperties: PropertiesTableProps = {
visibleWhen: {
doc: 'Provide a `path` or `itemPath` and a `hasValue` method that returns a boolean or the excepted value in order to show children. The first parameter is the value of the path. You can also use `isValid` instead of `hasValue` to only show the children when the field has no errors and has lost focus (blurred). You can change that behavior by using the `continuousValidation` property.',
type: 'object',
status: 'optional',
},
visibleWhenNot: {
doc: 'Same as `visibleWhen`, but with inverted logic.',
type: 'object',
status: 'optional',
},
pathDefined: {
doc: 'Given data context path must be defined to show children.',
type: 'string',
Expand Down Expand Up @@ -31,16 +41,6 @@ export const VisibilityProperties: PropertiesTableProps = {
type: 'string',
status: 'optional',
},
visibleWhen: {
doc: 'Provide a `path` or `itemPath` and a `hasValue` method that returns a boolean or the excepted value in order to show children. The first parameter is the value of the path.',
type: 'object',
status: 'optional',
},
visibleWhenNot: {
doc: 'Same as `visibleWhen`, but with inverted logic.',
type: 'object',
status: 'optional',
},
inferData: {
doc: 'Will be called to decide by external logic, and show/hide contents based on the return value.',
type: 'function',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import React from 'react'
import { render, screen } from '@testing-library/react'
import { fireEvent, render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { FilterData, Provider } from '../../../DataContext'
import Visibility from '../Visibility'
import useVisibility from '../useVisibility'
import { Field, Form, Iterate } from '../../..'
import { Flex } from '../../../../../components'
import { P } from '../../../../../elements'
Expand Down Expand Up @@ -944,4 +945,94 @@ describe('Visibility', () => {
expect(screen.getByText('Child')).toBeInTheDocument()
})
})

describe('visibleWhen with "isValid"', () => {
it('should return only false when field path is non existent', () => {
const collectResult = []

const MockComponent = () => {
const result = useVisibility().check({
visibleWhen: {
path: '/non-existent-path',
isValid: true,
},
})
collectResult.push(result)
return null
}

render(
<Provider>
<MockComponent />
</Provider>
)

expect(collectResult).toEqual([false])
})

it('should return only false on first render', () => {
const collectResult = []

const MockComponent = () => {
const result = useVisibility().check({
visibleWhen: {
path: '/myPath',
isValid: true,
},
})
collectResult.push(result)
return null
}

render(
<Provider>
<Field.Number path="/myPath" required minimum={2} />
<MockComponent />
</Provider>
)

expect(collectResult).toEqual([false, false, false])

fireEvent.focus(document.querySelector('input'))
fireEvent.change(document.querySelector('input'), {
target: { value: '2' },
})
expect(collectResult).toEqual([false, false, false, false])

fireEvent.blur(document.querySelector('input'))
expect(collectResult).toEqual([false, false, false, false, true])
})

it('should support fields without focus and blur events', async () => {
const collectResult = []

const MockComponent = () => {
const result = useVisibility().check({
visibleWhen: {
path: '/myPath',
isValid: true,
},
})
collectResult.push(result)
return null
}

render(
<Provider>
<Field.Boolean path="/myPath" required />
<MockComponent />
</Provider>
)

expect(collectResult).toEqual([false, false, false])

await userEvent.click(document.querySelector('input'))
expect(collectResult).toEqual([false, false, false, true])

// Should have no effect
fireEvent.focus(document.querySelector('input'))
fireEvent.blur(document.querySelector('input'))
expect(collectResult).toEqual([false, false, false, true])
})
})
})
Loading
Loading