From 9dd8402e366aa4008617cdffc15a1278dbe7afc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20H=C3=B8egh?= Date: Fri, 6 Dec 2024 10:14:53 +0100 Subject: [PATCH] feat(Forms): add `Form.InfoOverlay` to display error, success (receipt), or custom messages to users (#4357) PR [Preview](https://eufemia-git-feat-forms-status-eufemia.vercel.app/uilib/extensions/forms/Form/InfoOverlay/). --------- Co-authored-by: Anders --- .../components/global-status/Examples.tsx | 4 +- .../uilib/components/global-status/demos.mdx | 8 +- .../extensions/forms/Form/InfoOverlay.mdx | 25 ++ .../forms/Form/InfoOverlay/Examples.tsx | 120 ++++++ .../forms/Form/InfoOverlay/demos.mdx | 20 + .../forms/Form/InfoOverlay/info.mdx | 111 +++++ .../forms/Form/InfoOverlay/properties.mdx | 27 ++ .../height-animation/HeightAnimation.tsx | 2 +- .../extensions/forms/Form/Element/Element.tsx | 2 +- .../forms/Form/InfoOverlay/InfoOverlay.tsx | 202 +++++++++ .../Form/InfoOverlay/InfoOverlayDocs.tsx | 62 +++ .../__tests__/InfoOverlay.test.tsx | 405 ++++++++++++++++++ .../forms/Form/InfoOverlay/index.ts | 2 + .../forms/Form/InfoOverlay/setContent.ts | 18 + .../stories/InfoOverlay.stories.tsx | 71 +++ .../src/extensions/forms/Form/index.ts | 1 + .../forms/Wizard/style/dnb-wizard-layout.scss | 2 +- .../forms/constants/locales/en-GB.ts | 12 + .../forms/constants/locales/nb-NO.ts | 13 + 19 files changed, 1098 insertions(+), 9 deletions(-) create mode 100644 packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/InfoOverlay.mdx create mode 100644 packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/InfoOverlay/Examples.tsx create mode 100644 packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/InfoOverlay/demos.mdx create mode 100644 packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/InfoOverlay/info.mdx create mode 100644 packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/InfoOverlay/properties.mdx create mode 100644 packages/dnb-eufemia/src/extensions/forms/Form/InfoOverlay/InfoOverlay.tsx create mode 100644 packages/dnb-eufemia/src/extensions/forms/Form/InfoOverlay/InfoOverlayDocs.tsx create mode 100644 packages/dnb-eufemia/src/extensions/forms/Form/InfoOverlay/__tests__/InfoOverlay.test.tsx create mode 100644 packages/dnb-eufemia/src/extensions/forms/Form/InfoOverlay/index.ts create mode 100644 packages/dnb-eufemia/src/extensions/forms/Form/InfoOverlay/setContent.ts create mode 100644 packages/dnb-eufemia/src/extensions/forms/Form/InfoOverlay/stories/InfoOverlay.stories.tsx diff --git a/packages/dnb-design-system-portal/src/docs/uilib/components/global-status/Examples.tsx b/packages/dnb-design-system-portal/src/docs/uilib/components/global-status/Examples.tsx index 0f8b130ea27..6f7e663d319 100644 --- a/packages/dnb-design-system-portal/src/docs/uilib/components/global-status/Examples.tsx +++ b/packages/dnb-design-system-portal/src/docs/uilib/components/global-status/Examples.tsx @@ -14,7 +14,7 @@ import { } from '@dnb/eufemia/src' import { Provider } from '@dnb/eufemia/src/shared' -export const GlobalStatusError = () => ( +export const GlobalInfoOverlayError = () => ( ( ) -export const GlobalStatusSuccess = () => ( +export const GlobalInfoOverlaySuccess = () => ( + ### GlobalStatus displaying info status @@ -31,7 +31,7 @@ import { ### GlobalStatus displaying success status - + ### To showcase the automated coupling between **FormStatus** and **GlobalStatus** diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/InfoOverlay.mdx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/InfoOverlay.mdx new file mode 100644 index 00000000000..6d815dc5072 --- /dev/null +++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/InfoOverlay.mdx @@ -0,0 +1,25 @@ +--- +title: 'InfoOverlay' +description: '`Form.InfoOverlay` is used to display an informational message that fully covers the available space.' +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: InfoOverlay + href: /uilib/extensions/forms/Form/InfoOverlay +--- + +import Info from 'Docs/uilib/extensions/forms/Form/InfoOverlay/info' +import Demos from 'Docs/uilib/extensions/forms/Form/InfoOverlay/demos' + + + diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/InfoOverlay/Examples.tsx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/InfoOverlay/Examples.tsx new file mode 100644 index 00000000000..f5a4401488a --- /dev/null +++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/InfoOverlay/Examples.tsx @@ -0,0 +1,120 @@ +import ComponentBox from '../../../../../../shared/tags/ComponentBox' +import { createRequest } from '../SubmitIndicator/Examples' +import { Field, Form, Wizard } from '@dnb/eufemia/src/extensions/forms' +import { Button } from '@dnb/eufemia/src' + +const request = createRequest() + +export const ErrorMessage = () => { + return ( + + {() => { + // myFormId can be anything, as long as it's a unique instance + const myFormId = () => null + + return ( + { + await request(1000) // Simulate a request + + Form.InfoOverlay.setContent(myFormId, 'error') + }} + > + + + + + + + + + + + ) + }} + + ) +} + +export const SuccessMessage = () => { + return ( + + {() => { + // myFormId can be anything, as long as it's a unique instance + const myFormId = () => null + + return ( + { + await request(1000) // Simulate a request + + Form.InfoOverlay.setContent(myFormId, 'success') + }} + > + + + + + + + + ) + }} + + ) +} + +export const WithAWizard = () => { + const request = createRequest() + return ( + + {() => { + // myFormId can be anything, as long as it's a unique instance + const myFormId = () => null + + return ( + { + await request(1000) + Form.InfoOverlay.setContent(myFormId, 'success') + }} + > + + { + await request(1000) + }} + > + + + + + + + + + + + + + + + + ) + }} + + ) +} diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/InfoOverlay/demos.mdx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/InfoOverlay/demos.mdx new file mode 100644 index 00000000000..40073d16691 --- /dev/null +++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/InfoOverlay/demos.mdx @@ -0,0 +1,20 @@ +--- +showTabs: true +hideInMenu: true +--- + +import * as Examples from './Examples' + +## Demos + +### Error message + + + +### Success message + + + +### With a Wizard + + diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/InfoOverlay/info.mdx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/InfoOverlay/info.mdx new file mode 100644 index 00000000000..bd63178e998 --- /dev/null +++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/InfoOverlay/info.mdx @@ -0,0 +1,111 @@ +--- +showTabs: true +hideInMenu: true +--- + +## Description + +`Form.InfoOverlay` is used to display an informational message that fully covers the available space. It can show a custom message or content, a `success` message as a receipt, or an `error` message to indicate an issue. + +## Usage + +By default the given children will be shown. + +```tsx +import { Form } from '@dnb/eufemia/extensions/forms' + +render( + + visible content + , +) +``` + +## Display a message + +There are two ways to display a message: + +- Using the `Form.InfoOverlay.setContent` method. +- Using the `content` prop. + +### Using the `Form.InfoOverlay.setContent` method + +You can show the success or error message by using the `Form.InfoOverlay.setContent` method: + +```tsx +Form.InfoOverlay.setContent(myId, <>info content) +// or +Form.InfoOverlay.setContent(myId, 'success') +// or +Form.InfoOverlay.setContent(myId, 'error') +``` + +And render the component with an `id` prop: + +```tsx +content +``` + +You can call it whenever you need to show the success message. Here is an example of how to use it. + +**Note:** the `id` prop is inherited from the `Form.Handler` component in this example. + +```tsx +import { Form } from '@dnb/eufemia/extensions/forms' + +// myFormId can be anything, as long as it's a unique instance +const myFormId = () => null + +render( + { + // 1. Send the request + + // 2. Show the success message + Form.InfoOverlay.setContent(myFormId, 'success') + }} + > + fallback content + , +) +``` + +### Using the `content` prop + +You can show the success or error message by using the `content` prop: + +```tsx +info content}>fallback content +fallback content +fallback content +``` + +## Customization of the `success` and `error` messages + +You can customize the `success` and `error` messages by using the `success` and `error` props. + +```tsx + {}, + }} + error={{ + title: 'Custom title', + description: 'Custom description', + cancelButton: 'Custom cancel', + retryButton: 'Custom retry', + retryingText: 'Custom retrying text', + }} +> + fallback content + +``` + +## Accessibility + +The component will manage focus handling, which is important for screen readers and users using keyboard navigation. diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/InfoOverlay/properties.mdx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/InfoOverlay/properties.mdx new file mode 100644 index 00000000000..ebf75180e9d --- /dev/null +++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/InfoOverlay/properties.mdx @@ -0,0 +1,27 @@ +--- +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 { + InfoOverlaySuccessProperties, + InfoOverlayErrorProperties, +} from '@dnb/eufemia/src/extensions/forms/Form/InfoOverlay/InfoOverlayDocs' + +## Properties + +### Error + + + +### Success + + + +## Translations + + diff --git a/packages/dnb-eufemia/src/components/height-animation/HeightAnimation.tsx b/packages/dnb-eufemia/src/components/height-animation/HeightAnimation.tsx index ec0781c9a16..f8feff01647 100644 --- a/packages/dnb-eufemia/src/components/height-animation/HeightAnimation.tsx +++ b/packages/dnb-eufemia/src/components/height-animation/HeightAnimation.tsx @@ -48,7 +48,7 @@ export type HeightAnimationProps = { export type HeightAnimationAllProps = HeightAnimationProps & SpacingProps & - Omit, 'ref'> + Omit, 'ref' | 'onAnimationEnd'> function HeightAnimation({ open = true, diff --git a/packages/dnb-eufemia/src/extensions/forms/Form/Element/Element.tsx b/packages/dnb-eufemia/src/extensions/forms/Form/Element/Element.tsx index 74344c0af3c..462dec3e8f3 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Form/Element/Element.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/Form/Element/Element.tsx @@ -78,7 +78,7 @@ export default function FormElement(props: Props) { key={key} state={key} id={`${id}-form-status-${key}`} - className="dnb-forms-status" + className="dnb-forms-form__status-message" show={Boolean(value)} no_animation={false} shellSpace={{ top: 'small' }} diff --git a/packages/dnb-eufemia/src/extensions/forms/Form/InfoOverlay/InfoOverlay.tsx b/packages/dnb-eufemia/src/extensions/forms/Form/InfoOverlay/InfoOverlay.tsx new file mode 100644 index 00000000000..6f1a82c1542 --- /dev/null +++ b/packages/dnb-eufemia/src/extensions/forms/Form/InfoOverlay/InfoOverlay.tsx @@ -0,0 +1,202 @@ +import React, { useCallback, useContext, useRef } from 'react' +import classnames from 'classnames' +import Visibility from '../Visibility' +import DataContext from '../../DataContext/Context' +import { + SharedStateId, + useSharedState, +} from '../../../../shared/helpers/useSharedState' +import useMounted from '../../../../shared/helpers/useMounted' +import setContent, { InfoOverlayContent } from './setContent' +import { + Button, + Flex, + HeightAnimation, + Section, +} from '../../../../components' +import { HeightAnimationAllProps } from '../../../../components/HeightAnimation' +import { P } from '../../../../elements' +import { useTranslation } from '../../hooks' +import MainHeading from '../MainHeading' +import SubmitButton from '../SubmitButton' + +export type Props = { + /** + * The content to show. + * If not given, the children will be shown. + * Can be `success`, `error` or a custom content. + */ + content?: InfoOverlayContent + onCancel?: () => void + + /** Predefined content */ + success?: { + title?: React.ReactNode + description?: React.ReactNode + buttonText?: React.ReactNode + buttonHref?: string + buttonClickHandler?: () => void + } + /** Predefined content */ + error?: { + title?: React.ReactNode + description?: React.ReactNode + retryButton?: React.ReactNode + cancelButton?: React.ReactNode + } + + // Various props + id?: SharedStateId + children: React.ReactNode + className?: string +} + +function InfoOverlay(props: Props) { + const { id: idProp, formState } = useContext(DataContext) + + const { + id = idProp, + content: contentProp, + success, + error, + onCancel, + className, + children, + ...restProps + } = props + + const { data } = useSharedState<{ + content?: InfoOverlayContent + }>(id) + const { content = contentProp } = data || {} + + const translations = useTranslation() + const mountedRef = useMounted() + const innerRef = useRef(null) + const onAnimationEnd: HeightAnimationAllProps['onAnimationEnd'] = + useCallback( + (state) => { + if (mountedRef.current && state === 'opened') { + innerRef.current.focus?.() + } + }, + [mountedRef] + ) + + // To keep the content visible while hiding it with the HightAnimation + const currentContentRef = useRef() + if (content) { + currentContentRef.current = content + } + + const onCancelHandler = useCallback(() => { + if (id) { + setContent(id, undefined) + } + onCancel?.() + }, [id, onCancel]) + + const childrenAreVisible = + typeof content !== 'undefined' ? !(content === content) : undefined + const statusContentIsVisible = + typeof content !== 'undefined' ? content === content : false + const status = + typeof content === 'string' && !content.includes(' ') + ? content + : undefined + + let statusContent = content + + if (currentContentRef.current === 'success') { + const tr = translations.InfoOverlaySuccess + const { + title, + description, + buttonText, + buttonHref, + buttonClickHandler, + } = success || {} + + statusContent = ( +
+ + {title ?? tr.title} +

{description ?? tr.description}

+ +
+
+ ) + } else if (currentContentRef.current === 'error') { + const tr = translations.InfoOverlayError + const { title, description, cancelButton, retryButton } = error || {} + + statusContent = ( +
+ + {title ?? tr.title} + +

+ {formState === 'pending' + ? tr.retryingText + : description ?? tr.description} +

+
+ + + {retryButton ?? tr.retryButton} + +
+
+ ) + } + + return ( +
+ + {statusContent} + + + + {children} + +
+ ) +} + +InfoOverlay.setContent = setContent +InfoOverlay._supportsSpacingProps = true + +export default InfoOverlay diff --git a/packages/dnb-eufemia/src/extensions/forms/Form/InfoOverlay/InfoOverlayDocs.tsx b/packages/dnb-eufemia/src/extensions/forms/Form/InfoOverlay/InfoOverlayDocs.tsx new file mode 100644 index 00000000000..e7d16cbef72 --- /dev/null +++ b/packages/dnb-eufemia/src/extensions/forms/Form/InfoOverlay/InfoOverlayDocs.tsx @@ -0,0 +1,62 @@ +import { PropertiesTableProps } from '../../../../shared/types' + +export const InfoOverlaySuccessProperties: PropertiesTableProps = { + title: { + doc: 'The title of the component.', + type: 'React.Node', + status: 'optional', + }, + description: { + doc: 'The description of the component.', + type: 'React.Node', + status: 'optional', + }, + buttonText: { + doc: 'The text of the button.', + type: 'React.Node', + status: 'optional', + }, + buttonHref: { + doc: 'The href of the button.', + type: 'string', + status: 'optional', + }, + buttonClickHandler: { + doc: 'The click handler of the button.', + type: 'function', + status: 'optional', + }, + '[Section](/uilib/components/section/properties)': { + doc: 'All Section properties.', + type: 'various', + status: 'optional', + }, +} + +export const InfoOverlayErrorProperties: PropertiesTableProps = { + title: { + doc: 'The title of the component.', + type: 'React.Node', + status: 'optional', + }, + description: { + doc: 'The description of the component.', + type: 'React.Node', + status: 'optional', + }, + cancelButton: { + doc: 'The text of the cancel button.', + type: 'React.Node', + status: 'optional', + }, + retryButton: { + doc: 'The text of the retry button.', + type: 'React.Node', + status: 'optional', + }, + '[Section](/uilib/components/section/properties)': { + doc: 'All Section properties.', + type: 'various', + status: 'optional', + }, +} diff --git a/packages/dnb-eufemia/src/extensions/forms/Form/InfoOverlay/__tests__/InfoOverlay.test.tsx b/packages/dnb-eufemia/src/extensions/forms/Form/InfoOverlay/__tests__/InfoOverlay.test.tsx new file mode 100644 index 00000000000..2043b561060 --- /dev/null +++ b/packages/dnb-eufemia/src/extensions/forms/Form/InfoOverlay/__tests__/InfoOverlay.test.tsx @@ -0,0 +1,405 @@ +import React from 'react' +import { act, fireEvent, render } from '@testing-library/react' +import nbNO from '../../../constants/locales/nb-NO' +import { Form } from '../../..' + +describe('Form.InfoOverlay', () => { + it('should render success with correct text', () => { + const nb = nbNO['nb-NO'].InfoOverlaySuccess + const formId = {} + + render( + + fallback content + + ) + + expect(document.querySelector('h2')).toBeNull() + expect(document.querySelector('p')).toBeNull() + + act(() => { + Form.InfoOverlay.setContent(formId, 'success') + }) + + expect(document.querySelector('h2')).toHaveTextContent(nb.title) + expect(document.querySelector('p')).toHaveTextContent(nb.description) + + const anchors = document.querySelectorAll('a') + expect(anchors).toHaveLength(1) + + const [anchor] = Array.from(anchors) + expect(anchor).toHaveTextContent(nb.buttonText) + expect(anchor).toHaveAttribute('href', '/') + }) + + it('should render error with correct text', async () => { + const nb = nbNO['nb-NO'].InfoOverlayError + const formId = {} + + render( + + fallback content + + ) + + expect(document.querySelector('h2')).toBeNull() + expect(document.querySelector('p')).toBeNull() + + act(() => { + Form.InfoOverlay.setContent(formId, 'error') + }) + + expect(document.querySelector('h2')).toHaveTextContent(nb.title) + expect(document.querySelector('p')).toHaveTextContent(nb.description) + + const buttons = document.querySelectorAll('button') + expect(buttons).toHaveLength(2) + + const [backButton, retryButton] = Array.from(buttons) + expect(backButton).toHaveTextContent(nb.cancelButton) + expect(retryButton).toHaveTextContent(nb.retryButton) + }) + + it('should accept custom buttonHref', () => { + const formId = {} + + render( + + + fallback content + + + ) + + act(() => { + Form.InfoOverlay.setContent(formId, 'success') + }) + + const anchor = document.querySelector('a') + expect(anchor).toHaveAttribute('href', 'http://custom') + }) + + it('should disable href when buttonClickHandler is given', () => { + const formId = {} + const buttonClickHandler = jest.fn() + + render( + + + fallback content + + + ) + + act(() => { + Form.InfoOverlay.setContent(formId, 'success') + }) + + const button = document.querySelector('button') + fireEvent.click(button) + + expect(button).not.toHaveAttribute('href') + expect(buttonClickHandler).toHaveBeenCalledTimes(1) + }) + + it('should render success with custom text', () => { + const formId = {} + + render( + + + fallback content + + + ) + + expect(document.querySelector('h2')).toBeNull() + expect(document.querySelector('p')).toBeNull() + + act(() => { + Form.InfoOverlay.setContent(formId, 'success') + }) + + expect(document.querySelector('h2')).toHaveTextContent('Custom title') + expect(document.querySelector('p')).toHaveTextContent( + 'Custom description' + ) + + const anchors = document.querySelectorAll('a') + expect(anchors).toHaveLength(1) + + const [anchor] = Array.from(anchors) + expect(anchor).toHaveTextContent('Custom button text') + expect(anchor).toHaveAttribute('href', '/') + }) + + it('should render error with custom text', async () => { + const formId = {} + + render( + + + fallback content + + + ) + + expect(document.querySelector('h2')).toBeNull() + expect(document.querySelector('p')).toBeNull() + + act(() => { + Form.InfoOverlay.setContent(formId, 'error') + }) + + expect(document.querySelector('h2')).toHaveTextContent('Custom title') + expect(document.querySelector('p')).toHaveTextContent( + 'Custom description' + ) + + const buttons = document.querySelectorAll('button') + expect(buttons).toHaveLength(2) + + const [backButton, retryButton] = Array.from(buttons) + expect(backButton).toHaveTextContent('Custom cancel') + expect(retryButton).toHaveTextContent('Custom retry') + }) + + it('should keep children in the DOM', () => { + const formId = {} + + render( + + + content + + + ) + + expect(document.querySelector('h2')).toBeNull() + expect(document.querySelector('output')).toHaveTextContent('content') + + act(() => { + Form.InfoOverlay.setContent(formId, 'success') + }) + + expect(document.querySelector('h2')).toBeInTheDocument() + expect(document.querySelector('output')).toHaveTextContent('content') + }) + + it('should support "id" prop', () => { + const formId = {} + + render( + fallback content + ) + + const element = document.querySelector('.dnb-forms-info-overlay') + expect(element).not.toHaveTextContent('custom content') + + act(() => { + Form.InfoOverlay.setContent(formId, 'custom content') + }) + expect(element).toHaveTextContent('custom content') + }) + + it('"id" prop should take precedence over Form.Handler id', () => { + const formId = {} + + render( + + fallback content + + ) + + const element = document.querySelector('.dnb-forms-info-overlay') + expect(element).not.toHaveTextContent('custom content') + + act(() => { + Form.InfoOverlay.setContent(formId, 'custom content') + }) + expect(element).toHaveTextContent('custom content') + }) + + it('"setContent" should take precedence over content prop', () => { + const formId = {} + + render( + + never shown + + ) + + const element = document.querySelector('.dnb-forms-info-overlay') + expect(element).toHaveTextContent('other content') + + act(() => { + Form.InfoOverlay.setContent(formId, 'custom content') + }) + expect(element).toHaveTextContent('custom content') + }) + + it('should support "content" prop', () => { + render( + + never shown + + ) + + const element = document.querySelector('.dnb-forms-info-overlay') + expect(element).toHaveTextContent('custom content') + }) + + it('should not set class of "--*"', () => { + render( + + never shown + + ) + + const element = document.querySelector('.dnb-forms-info-overlay') + expect(element.className).toBe('dnb-forms-info-overlay dnb-no-focus') + }) + + it('should set class of "--success"', () => { + render( + never shown + ) + + const element = document.querySelector('.dnb-forms-info-overlay') + expect(element).toHaveClass('dnb-forms-info-overlay--success') + }) + + it('should set class of "--error"', () => { + render( + never shown + ) + + const element = document.querySelector('.dnb-forms-info-overlay') + expect(element).toHaveClass('dnb-forms-info-overlay--error') + }) + + it('should handle focus', () => { + const formId = {} + + render( + + + content + + + ) + + expect(document.querySelector('body')).toHaveFocus() + + act(() => { + Form.InfoOverlay.setContent(formId, 'success') + }) + + expect( + document.querySelector('.dnb-forms-info-overlay--success') + ).toHaveFocus() + + act(() => { + document.querySelector('body').focus() + Form.InfoOverlay.setContent(formId, undefined) + }) + + expect(document.querySelector('.dnb-forms-info-overlay')).toHaveFocus() + expect( + document.querySelector('.dnb-forms-info-overlay') + ).not.toHaveClass('dnb-forms-info-overlay--success') + + act(() => { + document.querySelector('body').focus() + Form.InfoOverlay.setContent(formId, 'success') + }) + + expect( + document.querySelector('.dnb-forms-info-overlay--success') + ).toHaveFocus() + expect(document.querySelector('.dnb-forms-info-overlay')).toHaveClass( + 'dnb-no-focus' + ) + expect( + document.querySelector('.dnb-forms-info-overlay') + ).toHaveAttribute('tabindex', '-1') + }) + + it('should show content when cancel button is clicked', () => { + const formId = {} + + render( + + + content + + + ) + + act(() => { + Form.InfoOverlay.setContent(formId, 'error') + }) + + expect(document.querySelector('.dnb-forms-info-overlay')).toHaveClass( + 'dnb-forms-info-overlay--error' + ) + + const buttons = document.querySelectorAll('button') + expect(buttons).toHaveLength(2) + + const [backButton] = Array.from(buttons) + + fireEvent.click(backButton) + + expect( + document.querySelector('.dnb-forms-info-overlay') + ).not.toHaveClass('dnb-forms-info-overlay--error') + }) + + it('should call onCancel when clicking on cancel button', () => { + const formId = {} + const onCancel = jest.fn() + + render( + + + content + + + ) + + act(() => { + Form.InfoOverlay.setContent(formId, 'error') + }) + + expect(document.querySelector('.dnb-forms-info-overlay')).toHaveClass( + 'dnb-forms-info-overlay--error' + ) + expect(onCancel).not.toHaveBeenCalled() + + const buttons = document.querySelectorAll('button') + expect(buttons).toHaveLength(2) + + const [backButton] = Array.from(buttons) + + fireEvent.click(backButton) + expect(onCancel).toHaveBeenCalledTimes(1) + }) +}) diff --git a/packages/dnb-eufemia/src/extensions/forms/Form/InfoOverlay/index.ts b/packages/dnb-eufemia/src/extensions/forms/Form/InfoOverlay/index.ts new file mode 100644 index 00000000000..d7affe5170a --- /dev/null +++ b/packages/dnb-eufemia/src/extensions/forms/Form/InfoOverlay/index.ts @@ -0,0 +1,2 @@ +export { default } from './InfoOverlay' +export * from './InfoOverlay' diff --git a/packages/dnb-eufemia/src/extensions/forms/Form/InfoOverlay/setContent.ts b/packages/dnb-eufemia/src/extensions/forms/Form/InfoOverlay/setContent.ts new file mode 100644 index 00000000000..32cef9c1c9b --- /dev/null +++ b/packages/dnb-eufemia/src/extensions/forms/Form/InfoOverlay/setContent.ts @@ -0,0 +1,18 @@ +import { + SharedStateId, + createSharedState, +} from '../../../../shared/helpers/useSharedState' + +export type InfoOverlayContent = + | 'success' + | 'error' + | React.ReactNode + | undefined + +export default function setContent( + id: SharedStateId, + content: InfoOverlayContent +) { + const sharedState = createSharedState(id) + sharedState.extend({ content }) +} diff --git a/packages/dnb-eufemia/src/extensions/forms/Form/InfoOverlay/stories/InfoOverlay.stories.tsx b/packages/dnb-eufemia/src/extensions/forms/Form/InfoOverlay/stories/InfoOverlay.stories.tsx new file mode 100644 index 00000000000..59afc2f03b3 --- /dev/null +++ b/packages/dnb-eufemia/src/extensions/forms/Form/InfoOverlay/stories/InfoOverlay.stories.tsx @@ -0,0 +1,71 @@ +import { Field, Form, Wizard } from '../../..' +import { Button } from '../../../../../components' + +export default { + title: 'Eufemia/Extensions/Forms/InfoOverlay', +} + +export function BothStatuses() { + const formId = () => null + return ( + <> + { + await new Promise((r) => setTimeout(r, 1000)) // Simulate a request + + Form.InfoOverlay.setContent(formId, 'success') + }} + > + + + + + + + + + + +
+ ----- Content ---- + + ) +} + +export function WithAWizard() { + return ( + { + await new Promise((r) => setTimeout(r, 1000)) + Form.InfoOverlay.setContent('unique-id', 'success') + console.log('data', data) + }} + > + + { + await new Promise((r) => setTimeout(r, 1000)) + }} + > + + + + + + + + + + + + ) +} diff --git a/packages/dnb-eufemia/src/extensions/forms/Form/index.ts b/packages/dnb-eufemia/src/extensions/forms/Form/index.ts index 3b2c6c245b6..ac25a3abde4 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Form/index.ts +++ b/packages/dnb-eufemia/src/extensions/forms/Form/index.ts @@ -11,6 +11,7 @@ export { default as MainHeading } from './MainHeading' export { default as SubHeading } from './SubHeading' export { default as Visibility } from './Visibility' export { default as Section } from './Section' +export { default as InfoOverlay } from './InfoOverlay' export { default as Card } from './Card' export { default as Isolation } from './Isolation' export { default as Snapshot } from './Snapshot' diff --git a/packages/dnb-eufemia/src/extensions/forms/Wizard/style/dnb-wizard-layout.scss b/packages/dnb-eufemia/src/extensions/forms/Wizard/style/dnb-wizard-layout.scss index 87a14b6a361..72603abdec1 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Wizard/style/dnb-wizard-layout.scss +++ b/packages/dnb-eufemia/src/extensions/forms/Wizard/style/dnb-wizard-layout.scss @@ -67,7 +67,7 @@ @include allAbove('medium') { .dnb-forms-form:has(.dnb-forms-wizard-layout--sidebar) - .dnb-forms-status { + .dnb-forms-form__status-message { margin-left: 21.5rem; } } diff --git a/packages/dnb-eufemia/src/extensions/forms/constants/locales/en-GB.ts b/packages/dnb-eufemia/src/extensions/forms/constants/locales/en-GB.ts index d2df5c1b137..b108ac91e13 100644 --- a/packages/dnb-eufemia/src/extensions/forms/constants/locales/en-GB.ts +++ b/packages/dnb-eufemia/src/extensions/forms/constants/locales/en-GB.ts @@ -35,6 +35,18 @@ export default { text: 'Remove', confirmRemoveText: 'Are you sure you want to delete this?', }, + InfoOverlaySuccess: { + title: 'Thank you', + description: 'We have received your information.', + buttonText: 'Back to homepage', + }, + InfoOverlayError: { + title: 'Sorry, something went wrong', + description: 'Please try again or contact us.', + cancelButton: 'Back', + retryButton: 'Try again', + retryingText: 'Retrying...', + }, SectionViewContainer: { editButton: 'Edit', }, diff --git a/packages/dnb-eufemia/src/extensions/forms/constants/locales/nb-NO.ts b/packages/dnb-eufemia/src/extensions/forms/constants/locales/nb-NO.ts index 1110f5caee2..f5c353f6a55 100644 --- a/packages/dnb-eufemia/src/extensions/forms/constants/locales/nb-NO.ts +++ b/packages/dnb-eufemia/src/extensions/forms/constants/locales/nb-NO.ts @@ -34,6 +34,19 @@ export default { text: 'Fjern', confirmRemoveText: 'Er du sikker på at du vil slette dette?', }, + InfoOverlaySuccess: { + title: 'Takk skal du ha', + description: 'Vi har mottatt din informasjon.', + buttonText: 'Tilbake til forsiden', + }, + InfoOverlayError: { + title: 'Beklager, noe gikk galt', + description: + 'Prøv igjen eller ta kontakt med oss om feilen vedstår.', + cancelButton: 'Tilbake', + retryButton: 'Prøv igjen', + retryingText: 'Prøver på nytt...', + }, SectionViewContainer: { editButton: 'Endre', },