Skip to content

Commit

Permalink
feat(Forms): introduce Form.Status components for showing error and…
Browse files Browse the repository at this point in the history
… success messages (receipt)

Co-authored-by: Anders <[email protected]>
  • Loading branch information
tujoworker and langz committed Dec 5, 2024
1 parent 2561acb commit 30d2b88
Show file tree
Hide file tree
Showing 14 changed files with 760 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
title: 'Status'
description: 'The `Form.Status` can be used to show success or error messages.'
showTabs: true
tabs:
- title: Info
key: '/info'
- title: Demos
key: '/demos'
- title: Properties
key: '/properties'
breadcrumb:
- text: Forms
href: /uilib/extensions/forms/
- text: Form
href: /uilib/extensions/forms/Form/
- text: Status
href: /uilib/extensions/forms/Form/Status
---

import Info from 'Docs/uilib/extensions/forms/Form/Status/info'
import Demos from 'Docs/uilib/extensions/forms/Form/Status/demos'

<Info />
<Demos />
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import ComponentBox from '../../../../../../shared/tags/ComponentBox'
import { Field, Form } from '@dnb/eufemia/src/extensions/forms'
import { createRequest } from '../SubmitIndicator/Examples'
import { Button } from '@dnb/eufemia/src'

const request = createRequest()

export const ErrorMessage = () => {
return (
<ComponentBox scope={{ request }}>
{() => {
// myFormId can be anything, as long as it's a unique instance
const myFormId = () => null

return (
<Form.Handler
id={myFormId}
onSubmit={async () => {
await request(1000) // Simulate a request

Form.Status.setStatus(myFormId, 'error')
}}
>
<Form.Status>
<Form.Card>
<Field.Email />
<Form.ButtonRow>
<Form.SubmitButton variant="send" />
<Button
variant="secondary"
onClick={() => {
Form.Status.setStatus(myFormId, 'error')
}}
>
Show error
</Button>
</Form.ButtonRow>
</Form.Card>
</Form.Status>
</Form.Handler>
)
}}
</ComponentBox>
)
}

export const SuccessMessage = () => {
return (
<ComponentBox scope={{ request }}>
{() => {
// myFormId can be anything, as long as it's a unique instance
const myFormId = () => null

return (
<Form.Handler
id={myFormId}
onSubmit={async () => {
await request(1000) // Simulate a request

Form.Status.setStatus(myFormId, 'success')
}}
>
<Form.Status>
<Form.Card>
<Field.Email />
<Form.SubmitButton variant="send" />
</Form.Card>
</Form.Status>
</Form.Handler>
)
}}
</ComponentBox>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
showTabs: true
hideInMenu: true
---

import * as Examples from './Examples'

## Demos

### Error message

<Examples.ErrorMessage />

### Success message

<Examples.SuccessMessage />
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
---
showTabs: true
hideInMenu: true
---

## Description

`Form.Status` displays a status message that is fully covering the available space and can be used to show success as a receipt or error messages.

## Usage

By default the given children will be shown.

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

render(
<Form.Handler>
<Form.Status>visible content</Form.Status>
</Form.Handler>,
)
```

## Showing the success or error message

You can show the success or error message by using the `Form.Status.setStatus` method:

```tsx
Form.Status.setStatus(id, 'success')
// or
Form.Status.setStatus(id, 'error')
```

You can call it whenever you need to show the success message. Here is an example of how to use it:

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

// myFormId can be anything, as long as it's a unique instance
const myFormId = () => null

render(
<Form.Handler
id={myFormId}
onSubmit={async () => {
Form.Status.setStatus(myFormId, 'success')
}}
>
<Form.Status>content</Form.Status>
</Form.Handler>,
)
```

## Accessibility

The component will manage focus handling, which is important for screen readers and users using keyboard navigation.
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
showTabs: true
hideInMenu: true
---

import TranslationsTable from 'dnb-design-system-portal/src/shared/parts/TranslationsTable'
import PropertiesTable from 'dnb-design-system-portal/src/shared/parts/PropertiesTable'
import {
StatusSuccessProperties,
StatusErrorProperties,
} from '@dnb/eufemia/src/extensions/forms/Form/Status/StatusDocs'

## Properties

### Error

<PropertiesTable props={StatusErrorProperties} />

### Success

<PropertiesTable props={StatusSuccessProperties} />

## Translations

<TranslationsTable localeKey={['StatusError', 'StatusSuccess']} />
167 changes: 167 additions & 0 deletions packages/dnb-eufemia/src/extensions/forms/Form/Status/Status.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import React, { useCallback, useContext, useEffect, useRef } from 'react'
import classnames from 'classnames'
import Visibility from '../Visibility'
import DataContext from '../../DataContext/Context'
import { useSharedState } from '../../../../shared/helpers/useSharedState'
import setStatus, { Status } from './setStatus'
import { Button, Flex, Section } from '../../../../components'
import { P } from '../../../../elements'
import { useTranslation } from '../../hooks'
import MainHeading from '../MainHeading'
import SubmitButton from '../SubmitButton'

export type Props = {
success?: {
title?: React.ReactNode
description?: React.ReactNode
buttonText?: React.ReactNode
buttonHref?: string
}
error?: {
title?: React.ReactNode
description?: React.ReactNode
retryButton?: React.ReactNode
cancelButton?: React.ReactNode
}
onCancel?: () => void
children: React.ReactNode
className?: string
}

function StatusContainer(props: Props) {
const { success, error, onCancel, className, children, ...restProps } =
props

const translations = useTranslation()

const { id } = useContext(DataContext) || {}

const { data } = useSharedState<{
activeStatus?: Status
}>(id)
const { activeStatus } = data || {}

// To ensure we not animate on first render.
// When there are several Examples rendered at the same time,
// the first one will animate on the first render.
const animateRef = useRef(undefined)
useEffect(() => {
animateRef.current = true
}, [])

const innerRef = useRef<HTMLDivElement>(null)
const onVisible = useCallback(() => {
if (animateRef.current) {
innerRef.current.focus?.()
}
}, [])

// To keep the content visible while hiding it with the HightAnimation
const currentStatusRef = useRef<Status>()
if (activeStatus) {
currentStatusRef.current = activeStatus
}

const onCancelHandler = useCallback(() => {
if (id) {
setStatus(id, undefined)
}
onCancel?.()
}, [id, onCancel])

const childrenAreVisible =
typeof activeStatus !== 'undefined'
? !(activeStatus === activeStatus)
: undefined
const statusContentIsVisible =
typeof activeStatus !== 'undefined'
? activeStatus === activeStatus
: false

let statusContent = null

if (currentStatusRef.current === 'success') {
const tr = translations.StatusSuccess
const {
title,
description,
buttonText,
buttonHref = '/',
} = success || {}

statusContent = (
<Section
variant="info"
innerSpace={{ top: 'large', bottom: 'xx-large' }}
{...restProps}
>
<Flex.Stack gap="large">
<MainHeading>{title ?? tr.title}</MainHeading>
<P>{description ?? tr.description}</P>
{buttonHref && (
<Button href={buttonHref}>
{buttonText ?? tr.buttonText}
</Button>
)}
</Flex.Stack>
</Section>
)
} else if (currentStatusRef.current === 'error') {
const tr = translations.StatusError
const { title, description, cancelButton, retryButton } = error || {}

statusContent = (
<Section
variant="transparent"
innerSpace={{ top: 'large', bottom: 'xx-large' }}
{...restProps}
>
<Flex.Stack gap="large">
<MainHeading>{title ?? tr.title}</MainHeading>
<P>{description ?? tr.description}</P>
<Flex.Horizontal>
<Button variant="secondary" onClick={onCancelHandler}>
{cancelButton ?? tr.cancelButton}
</Button>
<SubmitButton>{retryButton ?? tr.retryButton}</SubmitButton>
</Flex.Horizontal>
</Flex.Stack>
</Section>
)
}

return (
<div
className={classnames(
'dnb-forms-status',
activeStatus && `dnb-forms-status--${activeStatus}`,
'dnb-no-focus',
className
)}
tabIndex={-1}
ref={innerRef}
>
<Visibility
visible={statusContentIsVisible}
onVisible={onVisible}
animate={animateRef.current}
>
{statusContent}
</Visibility>

<Visibility
visible={childrenAreVisible}
onVisible={onVisible}
animate={animateRef.current}
keepInDOM
>
{children}
</Visibility>
</div>
)
}

StatusContainer.setStatus = setStatus
StatusContainer._supportsSpacingProps = true

export default StatusContainer
Loading

0 comments on commit 30d2b88

Please sign in to comment.