Skip to content

Commit

Permalink
feat(Forms): show optional label when a field uses required={false}
Browse files Browse the repository at this point in the history
… and add `labelSuffix` prop to each field (#3921)
  • Loading branch information
tujoworker authored Sep 12, 2024
1 parent 5c81848 commit 60e440a
Show file tree
Hide file tree
Showing 17 changed files with 454 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,26 @@ import { Button, Card, Flex, P, Section } from '@dnb/eufemia/src'
import { debounceAsync } from '@dnb/eufemia/src/shared/helpers/debounce'
import { createRequest } from '../SubmitIndicator/Examples'

export const RequiredAndOptionalFields = () => {
return (
<ComponentBox data-visual-test="required-and-optional-fields">
<Form.Handler required>
<Card stack>
<Field.Email path="/email" required={false} />
<Field.String
path="/custom"
label="Label"
labelDescription="\nLabel description"
required={false}
/>
<Field.Currency path="/amount" label="Amount" />
<Form.SubmitButton />
</Card>
</Form.Handler>
</ComponentBox>
)
}

export const AsyncSubmit = () => {
return (
<ComponentBox>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ import * as Examples from './Examples'

## Demos

### Required and Optional Fields

To make all fields required, set the `required` prop on the `Form.Handler` component.

For fields that should remain optional, use `required={false}` prop on the specific field. When doing so, it will append "(optional)" to the optional field's label(`labelSuffix`).

<Examples.RequiredAndOptionalFields />

### In combination with a SubmitButton

This example uses an async `onSubmit` event handler. It will disable all fields and show an indicator on the [SubmitButton](/uilib/extensions/forms/Form/SubmitButton/) while the form is pending.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,24 @@ The `required` property is a boolean that indicates whether the field is require
<Field.PhoneNumber required />
```

**NB:** You can also use the `required` property on the [Form.Handler](/uilib/extensions/forms/Form/Handler/) component, [Wizard.Step](/uilib/extensions/forms/Wizard/Step/) component or nested in the [Form.FieldProps](/uilib/extensions/forms/Form/FieldProps/) component.
**Note:** You can use the `required` property on the [Form.Handler](/uilib/extensions/forms/Form/Handler/) or [Wizard.Step](/uilib/extensions/forms/Wizard/Step/) components ([example](/uilib/extensions/forms/Form/Handler/demos/#required-and-optional-fields)). Additionally, the [Form.Section](/uilib/extensions/forms/Form/Section/) component as well as the [Form.FieldProps](/uilib/extensions/forms/Form/FieldProps/) provider has a `required` property, which will make all nested fields within that section required.

```tsx
<Form.Handler required>
<Field.PhoneNumber />
<Field.String />
</Form.Handler>
```

When you need to opt-out of the required field validation, you can use the `required={false}` property. This will also add a "(optional)" suffix to the field label(`labelSuffix`).

```tsx
<Form.Handler required>
<Field.String label="I'm required" />
<Field.String label="I'm not" required={false} />
<Field.Email required={false} labelSuffix="(recommended)" />
</Form.Handler>
```

#### pattern

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4226,6 +4226,97 @@ describe('DataContext.Provider', () => {
})
})

describe('required={false}', () => {
it('should add optional label', () => {
const { rerender } = render(
<DataContext.Provider required>
<Field.String label="Foo" />
<Field.String label="Bar" required={false} />
<Field.String label="Baz" />
</DataContext.Provider>
)

const [first, second, third] = Array.from(
document.querySelectorAll('label')
)

expect(first).toHaveTextContent('Foo')
expect(second).toHaveTextContent(
`Bar ${nb.Field.optionalLabelSuffix}`
)
expect(third).toHaveTextContent('Baz')

rerender(
<DataContext.Provider required>
<Field.String label="Foo" required={false} />
<Field.String label="Bar" />
<Field.String label="Baz" required={false} />
</DataContext.Provider>
)

expect(first).toHaveTextContent(
`Foo ${nb.Field.optionalLabelSuffix}`
)
expect(second).toHaveTextContent('Bar')
expect(third).toHaveTextContent(
`Baz ${nb.Field.optionalLabelSuffix}`
)
})

it('should prioritize labelSuffix over optionalLabel', () => {
render(
<DataContext.Provider required>
<Field.Email
label="e-post"
required={false}
labelSuffix="(suffix)"
/>
</DataContext.Provider>
)

const labelElement = document.querySelector('label')
expect(labelElement.textContent).toBe('e-post (suffix)')
})

it('should hide labelSuffix with empty string', () => {
render(
<DataContext.Provider required>
<Field.Email label="e-post" required={false} labelSuffix="" />
</DataContext.Provider>
)

const labelElement = document.querySelector('label')
expect(labelElement.textContent).toBe('e-post')
})

it('should support translations', () => {
render(
<DataContext.Provider
required
translations={{
'nb-NO': {
Field: {
optionalLabelSuffix: '(recommended)',
},
},
}}
>
<Field.String label="Foo" />
<Field.String label="Bar" required={false} />
<Field.String label="Baz" />
</DataContext.Provider>
)

const [first, second, third] = Array.from(
document.querySelectorAll('label')
)

expect(first).toHaveTextContent('Foo')
expect(second).toHaveTextContent('Bar (recommended)')
expect(third).toHaveTextContent('Baz')
})
})

describe('required', () => {
it('should make all fields required', () => {
const { rerender } = render(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,26 @@ describe('Field.Email', () => {
expect(input).toHaveAttribute('type', 'email')
})

it('should have default label', () => {
render(<Field.Email />)

const label = document.querySelector('label')
expect(label).toHaveTextContent(nb.Email.label)
})

it('should add (optional) text to the label if required={false}', () => {
render(
<Form.Handler required>
<Field.Email required={false} />
</Form.Handler>
)

const label = document.querySelector('label')
expect(label).toHaveTextContent(
`${nb.Email.label} ${nb.Field.optionalLabelSuffix}`
)
})

it('should allow a custom pattern', async () => {
render(<Field.Email pattern="[A-Z]" required />)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,40 @@ describe('Field.PhoneNumber', () => {
expect(labelElement()).not.toHaveAttribute('disabled')
})

it('should have default label', () => {
render(<Field.PhoneNumber />)

const label = document.querySelector('.dnb-forms-field-phone-number')
expect(label).toHaveTextContent(nbNO.PhoneNumber.label)
})

it('should add (optional) text to the number label if required={false}', () => {
render(
<Form.Handler required>
<Field.PhoneNumber required={false} />
</Form.Handler>
)

const codeElement = document.querySelector(
'.dnb-forms-field-phone-number__country-code'
) as HTMLInputElement
const numberElement = document.querySelector(
'.dnb-forms-field-phone-number__number'
) as HTMLInputElement

expect(codeElement.querySelector('label')).not.toHaveTextContent(
`${nbNO.Field.optionalLabelSuffix}`
)
expect(numberElement.querySelector('label')).toHaveTextContent(
`${nbNO.PhoneNumber.label} ${nbNO.Field.optionalLabelSuffix}`
)

// Use "textContent" to check against non-breaking space
expect(numberElement.querySelector('label').textContent).toBe(
`${nbNO.PhoneNumber.label}${' '}${nbNO.Field.optionalLabelSuffix}`
)
})

it('should only have a mask when +47 is given', async () => {
const { rerender } = render(<Field.PhoneNumber value="999999990000" />)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export type Props = Pick<
| keyof ComponentProps
| 'layout'
| 'label'
| 'labelSuffix'
| 'labelDescription'
| 'info'
| 'warning'
Expand All @@ -76,6 +77,8 @@ export type Props = Pick<
fieldState?: SubmitState
/** Typography size */
labelSize?: 'medium' | 'large'
/** For internal use only */
required?: boolean
children?: React.ReactNode
} & React.HTMLAttributes<HTMLDivElement>

Expand All @@ -93,8 +96,10 @@ function FieldBlock(props: Props) {
composition,
label: labelProp,
labelDescription,
labelSuffix,
labelSrOnly,
asFieldset,
required,
info,
warning,
error: errorProp,
Expand Down Expand Up @@ -122,15 +127,46 @@ function FieldBlock(props: Props) {
return Boolean(errorProp)
}, []) // eslint-disable-line react-hooks/exhaustive-deps

const { optionalLabelSuffix } = useTranslation().Field
const labelSuffixText = useMemo(() => {
if (required === false || typeof labelSuffix !== 'undefined') {
return labelSuffix ?? optionalLabelSuffix
}
return ''
}, [required, labelSuffix, optionalLabelSuffix])

const label = useMemo(() => {
let content = labelProp

if (iterateIndex !== undefined) {
return convertJsxToString(labelProp).replace(
content = convertJsxToString(labelProp).replace(
'{itemNr}',
String(iterateIndex + 1)
)
}
return labelProp
}, [iterateIndex, labelProp])

if (labelSuffixText) {
if (convertJsxToString(content).includes(optionalLabelSuffix)) {
return content
}

if (typeof content === 'string') {
return content + ' ' + labelSuffixText
}

if (React.isValidElement(content)) {
return (
<>
{content}
{' '}
{labelSuffixText}
</>
)
}
}

return content
}, [iterateIndex, labelProp, labelSuffixText])

const setInternalRecord = useCallback((props: StateBasis) => {
const { stateId, identifier, type } = props
Expand Down
Loading

0 comments on commit 60e440a

Please sign in to comment.