From d5498704b2f3529a13f79f8da437070ea1edce96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20H=C3=B8egh?= Date: Fri, 27 Sep 2024 22:53:30 +0200 Subject: [PATCH] feat(Forms): deprecate `Form.FieldProps` in favor of `Field.Provider` (#4020) --- .../releases/eufemia/v11-info.mdx | 8 +- .../extensions/forms/Form/FieldProps.mdx | 25 --- .../forms/Form/FieldProps/properties.mdx | 10 - .../docs/uilib/extensions/forms/changelog.mdx | 2 +- .../forms/feature-fields/Provider.mdx | 26 +++ .../Provider}/Examples.tsx | 12 +- .../Provider}/demos.mdx | 0 .../Provider}/info.mdx | 8 +- .../feature-fields/Provider/properties.mdx | 10 + .../extensions/forms/getting-started.mdx | 2 +- .../forms/DataContext/Provider/Provider.tsx | 2 +- .../forms/Field/Provider/FieldProvider.tsx | 48 +++++ .../Field/Provider/FieldProviderContext.ts | 16 ++ .../Provider/FieldProviderDocs.ts} | 2 +- .../__tests__/FieldProvider.test.tsx} | 181 ++++++++++++++---- .../__tests__/useFieldProvider.test.tsx | 89 +++++++++ .../extensions/forms/Field/Provider/index.ts | 2 + .../forms/Field/Provider/useFieldProvider.ts | 112 +++++++++++ .../src/extensions/forms/Field/index.ts | 1 + .../forms/Form/FieldProps/FieldProps.tsx | 141 -------------- .../Form/FieldProps/FieldPropsContext.ts | 15 -- .../extensions/forms/Form/FieldProps/index.ts | 2 - .../extensions/forms/Form/Section/Section.tsx | 2 +- .../Form/Section/__tests__/Section.test.tsx | 2 +- .../forms/Form/Visibility/Visibility.tsx | 6 +- .../src/extensions/forms/Form/index.ts | 9 +- .../src/extensions/forms/Wizard/Step/Step.tsx | 4 +- .../extensions/forms/hooks/useFieldProps.ts | 4 +- 28 files changed, 483 insertions(+), 258 deletions(-) delete mode 100644 packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/FieldProps.mdx delete mode 100644 packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/FieldProps/properties.mdx create mode 100644 packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/feature-fields/Provider.mdx rename packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/{Form/FieldProps => feature-fields/Provider}/Examples.tsx (87%) rename packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/{Form/FieldProps => feature-fields/Provider}/demos.mdx (100%) rename packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/{Form/FieldProps => feature-fields/Provider}/info.mdx (50%) create mode 100644 packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/feature-fields/Provider/properties.mdx create mode 100644 packages/dnb-eufemia/src/extensions/forms/Field/Provider/FieldProvider.tsx create mode 100644 packages/dnb-eufemia/src/extensions/forms/Field/Provider/FieldProviderContext.ts rename packages/dnb-eufemia/src/extensions/forms/{Form/FieldProps/FieldPropsDocs.ts => Field/Provider/FieldProviderDocs.ts} (84%) rename packages/dnb-eufemia/src/extensions/forms/{Form/FieldProps/__tests__/FieldProps.test.tsx => Field/Provider/__tests__/FieldProvider.test.tsx} (54%) create mode 100644 packages/dnb-eufemia/src/extensions/forms/Field/Provider/__tests__/useFieldProvider.test.tsx create mode 100644 packages/dnb-eufemia/src/extensions/forms/Field/Provider/index.ts create mode 100644 packages/dnb-eufemia/src/extensions/forms/Field/Provider/useFieldProvider.ts delete mode 100644 packages/dnb-eufemia/src/extensions/forms/Form/FieldProps/FieldProps.tsx delete mode 100644 packages/dnb-eufemia/src/extensions/forms/Form/FieldProps/FieldPropsContext.ts delete mode 100644 packages/dnb-eufemia/src/extensions/forms/Form/FieldProps/index.ts diff --git a/packages/dnb-design-system-portal/src/docs/uilib/about-the-lib/releases/eufemia/v11-info.mdx b/packages/dnb-design-system-portal/src/docs/uilib/about-the-lib/releases/eufemia/v11-info.mdx index 41521c0cbf1..6047990ef8d 100644 --- a/packages/dnb-design-system-portal/src/docs/uilib/about-the-lib/releases/eufemia/v11-info.mdx +++ b/packages/dnb-design-system-portal/src/docs/uilib/about-the-lib/releases/eufemia/v11-info.mdx @@ -132,13 +132,11 @@ The `InputPassword` component has been moved to `Field.Password`, and is now a p - replace `withValue` with `hasValue`. -## Form.useError +## Eufemia Forms - replace `useError` with `useValidation`. - -## Form.Iterate - -- Rename label variable `{itemNr}` to `{itemNo}`. +- replace Form.Iterate label variable `{itemNr}` with `{itemNo}`. +- replace `Form.FieldProps` with `Field.Provider`. ## NumberFormat diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/FieldProps.mdx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/FieldProps.mdx deleted file mode 100644 index 6513f4c641f..00000000000 --- a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/FieldProps.mdx +++ /dev/null @@ -1,25 +0,0 @@ ---- -title: 'FieldProps' -description: '`Form.FieldProps` is a provider for forwarding fields properties, such as `required` or `disabled` to all nested field components.' -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: FieldProps - href: /uilib/extensions/forms/Form/FieldProps ---- - -import Info from 'Docs/uilib/extensions/forms/Form/FieldProps/info' -import Demos from 'Docs/uilib/extensions/forms/Form/FieldProps/demos' - - - diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/FieldProps/properties.mdx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/FieldProps/properties.mdx deleted file mode 100644 index df1e7c05ed1..00000000000 --- a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/FieldProps/properties.mdx +++ /dev/null @@ -1,10 +0,0 @@ ---- -showTabs: true ---- - -import PropertiesTable from 'dnb-design-system-portal/src/shared/parts/PropertiesTable' -import { FieldPropsProperties } from '@dnb/eufemia/src/extensions/forms/Form/FieldProps/FieldPropsDocs' - -## Properties - - diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/changelog.mdx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/changelog.mdx index efbdd57d007..4e60560b772 100644 --- a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/changelog.mdx +++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/changelog.mdx @@ -80,7 +80,7 @@ Change log for the Eufemia Forms extension. ## v10.30 -- Added [Form.FieldProps](/uilib/extensions/forms/Form/FieldProps/) component to forward field properties, such as `required` or `disabled` to all nested field components. +- Added `Form.FieldProps` (which got renamed to [Field.Provider](/uilib/extensions/forms/feature-fields/Provider/)) component to forward field properties, such as `required` or `disabled` to all nested field components. - Added `locale` and `translations` to [Form.Handler](/uilib/extensions/forms/Form/Handler/) component to support custom translations. - Added `disabled` and `required` to [Form.Handler](/uilib/extensions/forms/Form/Handler/) component and pass these props to the children fields. - Added `fieldPropsWhenHidden` to [Form.Visibility](/uilib/extensions/forms/Form/Visibility/) component to pass props to the children when visibility is hidden. diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/feature-fields/Provider.mdx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/feature-fields/Provider.mdx new file mode 100644 index 00000000000..4e729b7c780 --- /dev/null +++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/feature-fields/Provider.mdx @@ -0,0 +1,26 @@ +--- +title: 'Field.Provider' +description: '`Field.Provider` is a provider for forwarding fields properties, such as `required` or `disabled` to all nested field components.' +hideInMenu: true +showTabs: true +tabs: + - title: Info + key: '/info' + - title: Demos + key: '/demos' + - title: Properties + key: '/properties' +breadcrumb: + - text: Forms + href: /uilib/extensions/forms/ + - text: Feature fields + href: /uilib/extensions/forms/feature-fields/ + - text: Field.Provider + href: /uilib/extensions/forms/feature-fields/Provider +--- + +import Info from 'Docs/uilib/extensions/forms/feature-fields/Provider/info' +import Demos from 'Docs/uilib/extensions/forms/feature-fields/Provider/demos' + + + diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/FieldProps/Examples.tsx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/feature-fields/Provider/Examples.tsx similarity index 87% rename from packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/FieldProps/Examples.tsx rename to packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/feature-fields/Provider/Examples.tsx index 52902b8713a..b67a2603886 100644 --- a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/FieldProps/Examples.tsx +++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/feature-fields/Provider/Examples.tsx @@ -9,10 +9,10 @@ export const Required = () => { - + - + @@ -30,14 +30,14 @@ export const Disabled = () => { - + - + @@ -51,14 +51,14 @@ export const Inverted = () => { - + - + diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/FieldProps/demos.mdx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/feature-fields/Provider/demos.mdx similarity index 100% rename from packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/FieldProps/demos.mdx rename to packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/feature-fields/Provider/demos.mdx diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/FieldProps/info.mdx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/feature-fields/Provider/info.mdx similarity index 50% rename from packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/FieldProps/info.mdx rename to packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/feature-fields/Provider/info.mdx index 97ca06be7c5..9ea686db66c 100644 --- a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/FieldProps/info.mdx +++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/feature-fields/Provider/info.mdx @@ -4,17 +4,17 @@ showTabs: true ## Description -`Form.FieldProps` is a provider for forwarding fields properties, such as `required` or `disabled` to all nested field components. +`Field.Provider` is a provider for forwarding fields properties, such as `required` or `disabled` to all nested field components. ## Usage ```tsx import { Form, Field } from '@dnb/eufemia/extensions/forms' render( - + - , + , ) ``` -Keep in mind, you can also set `required` or `disabled` on the [Form.Handler](/uilib/extensions/forms/Form/Handler/). And invert the logic via the `Form.FieldProps` by using `required={false}` or `disabled={false}`. +Keep in mind, you can also set `required` or `disabled` on the [Form.Handler](/uilib/extensions/forms/Form/Handler/). And invert the logic via the `Field.Provider` by using `required={false}` or `disabled={false}`. diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/feature-fields/Provider/properties.mdx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/feature-fields/Provider/properties.mdx new file mode 100644 index 00000000000..504c6029914 --- /dev/null +++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/feature-fields/Provider/properties.mdx @@ -0,0 +1,10 @@ +--- +showTabs: true +--- + +import PropertiesTable from 'dnb-design-system-portal/src/shared/parts/PropertiesTable' +import { FieldProviderProperties } from '@dnb/eufemia/src/extensions/forms/Field/Provider/FieldProviderDocs' + +## Properties + + diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/getting-started.mdx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/getting-started.mdx index 071facf2e9f..3d42ae7f0f7 100644 --- a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/getting-started.mdx +++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/getting-started.mdx @@ -411,7 +411,7 @@ The `required` property is a boolean that indicates whether the field is require ``` -**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. +**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 [Field.Provider](/uilib/extensions/forms/feature-fields/Provider/) provider has a `required` property, which will make all nested fields within that section required. ```tsx diff --git a/packages/dnb-eufemia/src/extensions/forms/DataContext/Provider/Provider.tsx b/packages/dnb-eufemia/src/extensions/forms/DataContext/Provider/Provider.tsx index 765012caaff..d82927a2910 100644 --- a/packages/dnb-eufemia/src/extensions/forms/DataContext/Provider/Provider.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/DataContext/Provider/Provider.tsx @@ -28,7 +28,7 @@ import { } from '../../types' import type { IsolationProviderProps } from '../../Form/Isolation/Isolation' import { debounce } from '../../../../shared/helpers' -import FieldPropsProvider from '../../Form/FieldProps' +import FieldPropsProvider from '../../Field/Provider' import useUpdateEffect from '../../../../shared/helpers/useUpdateEffect' import { isAsync } from '../../../../shared/helpers/isAsync' import { useSharedState } from '../../../../shared/helpers/useSharedState' diff --git a/packages/dnb-eufemia/src/extensions/forms/Field/Provider/FieldProvider.tsx b/packages/dnb-eufemia/src/extensions/forms/Field/Provider/FieldProvider.tsx new file mode 100644 index 00000000000..d9e5c14044b --- /dev/null +++ b/packages/dnb-eufemia/src/extensions/forms/Field/Provider/FieldProvider.tsx @@ -0,0 +1,48 @@ +import React from 'react' +import { Props as DataContextProps } from '../../DataContext/Provider' +import { FormStatusProps } from '../../../../components/FormStatus' +import FieldProviderContext from './FieldProviderContext' +import SharedProvider from '../../../../shared/Provider' +import { ContextProps } from '../../../../shared/Context' +import useFieldProvider from './useFieldProvider' +import { FieldProps, Path } from '../../types' + +export type FieldProviderProps = FieldProps & { + children: React.ReactNode + + /** + * Locale to use for all nested Eufemia components + */ + locale?: DataContextProps['locale'] + + /** + * Provide your own translations. Use the same format as defined in the translation files + */ + translations?: DataContextProps['translations'] + + /** For internal use only */ + overwriteProps?: { + [key: Path]: FieldProps + } + + /** For internal use only */ + formElement?: ContextProps['formElement'] + + /** For internal use only */ + FormStatus?: { globalStatus: FormStatusProps } +} + +function FieldProviderProvider(props: FieldProviderProps) { + const { children, ...restProps } = props + const { sharedProviderParams, ...providerValue } = + useFieldProvider(restProps) + + return ( + + {children} + + ) +} + +FieldProviderProvider._supportsSpacingProps = 'children' +export default FieldProviderProvider diff --git a/packages/dnb-eufemia/src/extensions/forms/Field/Provider/FieldProviderContext.ts b/packages/dnb-eufemia/src/extensions/forms/Field/Provider/FieldProviderContext.ts new file mode 100644 index 00000000000..677643207a6 --- /dev/null +++ b/packages/dnb-eufemia/src/extensions/forms/Field/Provider/FieldProviderContext.ts @@ -0,0 +1,16 @@ +import React from 'react' +import { UseFieldProps } from '../../types' + +export type FieldProviderContextProps = { + extend: (props: T) => T + inheritedProps?: UseFieldProps + inheritedContext?: UseFieldProps +} + +const extend: FieldProviderContextProps['extend'] = (props) => props +const FieldProviderContext = + React.createContext({ + extend, + }) + +export default FieldProviderContext diff --git a/packages/dnb-eufemia/src/extensions/forms/Form/FieldProps/FieldPropsDocs.ts b/packages/dnb-eufemia/src/extensions/forms/Field/Provider/FieldProviderDocs.ts similarity index 84% rename from packages/dnb-eufemia/src/extensions/forms/Form/FieldProps/FieldPropsDocs.ts rename to packages/dnb-eufemia/src/extensions/forms/Field/Provider/FieldProviderDocs.ts index 44edc9fab0a..789ff072943 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Form/FieldProps/FieldPropsDocs.ts +++ b/packages/dnb-eufemia/src/extensions/forms/Field/Provider/FieldProviderDocs.ts @@ -1,7 +1,7 @@ import { PropertiesTableProps } from '../../../../shared/types' import { dataValueProperties } from '../../hooks/DataValueDocs' -export const FieldPropsProperties: PropertiesTableProps = { +export const FieldProviderProperties: PropertiesTableProps = { required: dataValueProperties.required, disabled: dataValueProperties.disabled, locale: { diff --git a/packages/dnb-eufemia/src/extensions/forms/Form/FieldProps/__tests__/FieldProps.test.tsx b/packages/dnb-eufemia/src/extensions/forms/Field/Provider/__tests__/FieldProvider.test.tsx similarity index 54% rename from packages/dnb-eufemia/src/extensions/forms/Form/FieldProps/__tests__/FieldProps.test.tsx rename to packages/dnb-eufemia/src/extensions/forms/Field/Provider/__tests__/FieldProvider.test.tsx index 8aa7406b9d7..c8f58a83808 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Form/FieldProps/__tests__/FieldProps.test.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/Field/Provider/__tests__/FieldProvider.test.tsx @@ -1,22 +1,131 @@ import React from 'react' import { render } from '@testing-library/react' +import FieldProviderContext from '../FieldProviderContext' import { Form, Field } from '../../..' import nbNO from '../../../constants/locales/nb-NO' const nb = nbNO['nb-NO'] -describe('Form.FieldProps', () => { +describe('Field.Provider', () => { it('should have constant of _supportsSpacingProps="children"', () => { - expect(Form.FieldProps._supportsSpacingProps).toBe('children') + expect(Field.Provider._supportsSpacingProps).toBe('children') + }) + + it('should merge inheritedContext with props passed to extend', () => { + let collectedProps = null + + const Collector = (props) => { + return ( + + {({ extend }) => { + collectedProps = extend(props) + return null + }} + + ) + } + + render( + + + + ) + + expect(collectedProps).toEqual({ + disabled: true, + myProp: 'value', + }) + }) + + it('props passed to extend should override inheritedContext', () => { + let collectedProps = null + + const Collector = (props) => { + return ( + + {({ extend }) => { + collectedProps = extend(props) + return null + }} + + ) + } + + render( + + + + ) + + expect(collectedProps).toEqual({ + disabled: false, + myProp: 'value', + }) + }) + + it('props passed to extend should override nested inheritedContext', () => { + let collectedProps = null + + const Collector = (props) => { + return ( + + {({ extend }) => { + collectedProps = extend(props) + return null + }} + + ) + } + + render( + + + + + + ) + + expect(collectedProps).toEqual({ + disabled: true, + myProp: 'value', + }) + }) + + it('second provider should override nested inheritedContext', () => { + let collectedProps = null + + const Collector = (props) => { + return ( + + {({ extend }) => { + collectedProps = extend(props) + return null + }} + + ) + } + + render( + + + + + + ) + + expect(collectedProps).toEqual({ + disabled: false, + myProp: 'value', + }) }) describe('disable', () => { it('should disable the input and button', () => { render( - + - + ) const input = document.querySelector('input') @@ -28,10 +137,10 @@ describe('Form.FieldProps', () => { it('should not disable the input when prop is set', () => { render( - + - + ) const input = document.querySelector('input') @@ -44,10 +153,10 @@ describe('Form.FieldProps', () => { it('should handle the disabled prop from the Form.Handler', () => { const { rerender } = render( - + - + ) @@ -59,10 +168,10 @@ describe('Form.FieldProps', () => { rerender( - + - + ) @@ -71,10 +180,10 @@ describe('Form.FieldProps', () => { rerender( - + - + ) @@ -84,12 +193,12 @@ describe('Form.FieldProps', () => { it('should handle nested FieldProps', () => { const { rerender } = render( - - + + - - + + ) const input = document.querySelector('input') @@ -99,24 +208,24 @@ describe('Form.FieldProps', () => { expect(button).toBeDisabled() rerender( - - + + - - + + ) expect(input).not.toBeDisabled() expect(button).not.toBeDisabled() rerender( - - + + - - + + ) expect(input).not.toBeDisabled() @@ -125,10 +234,10 @@ describe('Form.FieldProps', () => { it('should support data-* attributes in fields', () => { render( - + - + ) const input = document.querySelector('input') @@ -142,9 +251,9 @@ describe('Form.FieldProps', () => { describe('require', () => { it('should require the input and button', () => { render( - + - + ) expect(document.querySelector('.dnb-form-status')).toHaveTextContent( @@ -154,9 +263,9 @@ describe('Form.FieldProps', () => { it('should not require the input when prop is set', () => { render( - + - + ) expect( @@ -167,9 +276,9 @@ describe('Form.FieldProps', () => { it('should handle the required prop from the Form.Handler', () => { const { rerender } = render( - + - + ) @@ -179,9 +288,9 @@ describe('Form.FieldProps', () => { rerender( - + - + ) @@ -191,9 +300,9 @@ describe('Form.FieldProps', () => { rerender( - + - + ) diff --git a/packages/dnb-eufemia/src/extensions/forms/Field/Provider/__tests__/useFieldProvider.test.tsx b/packages/dnb-eufemia/src/extensions/forms/Field/Provider/__tests__/useFieldProvider.test.tsx new file mode 100644 index 00000000000..50692763097 --- /dev/null +++ b/packages/dnb-eufemia/src/extensions/forms/Field/Provider/__tests__/useFieldProvider.test.tsx @@ -0,0 +1,89 @@ +import React from 'react' +import { renderHook } from '@testing-library/react' +import useFieldProvider from '../useFieldProvider' +import FieldProviderContext from '../FieldProviderContext' + +describe('useFieldProvider', () => { + it('should return extend function and inherited props', () => { + const props = { overwriteProps: {}, test: 'propField' } + const { result } = renderHook(useFieldProvider, { + initialProps: props, + }) + + expect(result.current.extend).toBeInstanceOf(Function) + expect(result.current.inheritedProps).toEqual({ test: 'propField' }) + expect(result.current.inheritedContext).toEqual({ + test: 'propField', + }) + }) + + it('extend function should merge overwriteProps correctly', () => { + const props = { + overwriteProps: { path: { value: 'overwriteField' } }, + test: 'propField', + } + const { result } = renderHook(useFieldProvider, { + initialProps: props, + }) + + const valueProps = { path: '/test/path', value: 'valueProps' } + + expect(result.current.extend(valueProps)).toEqual({ + path: '/test/path', + test: 'propField', + value: 'overwriteField', + }) + }) + + it('should pass inheritedContext to extend function', () => { + const props = { + overwriteProps: {}, + } + const inheritedContext = { disabled: true } + const inheritedProps = null + const extend = () => null + + const { result } = renderHook(useFieldProvider, { + initialProps: props, + wrapper: ({ children }) => ( + + {children} + + ), + }) + + const valueProps = {} + + expect(result.current.extend(valueProps)).toEqual({ + disabled: true, + }) + }) + + it('props passed to extend should override inheritedContext', () => { + const props = { + overwriteProps: {}, + } + const inheritedContext = { disabled: true } + const inheritedProps = null + const extend = () => null + + const { result } = renderHook(useFieldProvider, { + initialProps: props, + wrapper: ({ children }) => ( + + {children} + + ), + }) + + const valueProps = { disabled: false } + + expect(result.current.extend(valueProps)).toEqual({ + disabled: false, + }) + }) +}) diff --git a/packages/dnb-eufemia/src/extensions/forms/Field/Provider/index.ts b/packages/dnb-eufemia/src/extensions/forms/Field/Provider/index.ts new file mode 100644 index 00000000000..ccc0219cae3 --- /dev/null +++ b/packages/dnb-eufemia/src/extensions/forms/Field/Provider/index.ts @@ -0,0 +1,2 @@ +export { default } from './FieldProvider' +export * from './FieldProvider' diff --git a/packages/dnb-eufemia/src/extensions/forms/Field/Provider/useFieldProvider.ts b/packages/dnb-eufemia/src/extensions/forms/Field/Provider/useFieldProvider.ts new file mode 100644 index 00000000000..948906382d6 --- /dev/null +++ b/packages/dnb-eufemia/src/extensions/forms/Field/Provider/useFieldProvider.ts @@ -0,0 +1,112 @@ +import { useCallback, useContext, useMemo, useRef } from 'react' +import { + assignPropsWithContext, + extendDeep, +} from '../../../../shared/component-helper' +import FieldProviderContext from './FieldProviderContext' +import DataContext, { ContextState } from '../../DataContext/Context' +import SharedContext, { ContextProps } from '../../../../shared/Context' +import type { FieldProps } from '../../types' +import { FieldProviderProps } from './FieldProvider' + +function useFieldProvider(props?: Omit) { + const { formElement, FormStatus, overwriteProps, ...restProps } = + props || {} + const nestedContext = useContext(FieldProviderContext) + const inheritedProps = nestedContext?.inheritedContext + + const sharedContext = useContext(SharedContext) + const dataContextRef = useRef() + dataContextRef.current = useContext(DataContext) + + /** + * Always use data context as the last source for localization + */ + const locale = dataContextRef.current?.props?.locale ?? restProps?.locale + + const nestedFieldProps = useMemo(() => { + if (inheritedProps && Object.keys(inheritedProps).length > 0) { + return { ...inheritedProps, ...restProps } as FieldProps + } + + return restProps + }, [inheritedProps, restProps]) + + const sharedProviderParams: ContextProps = {} + + if (typeof nestedFieldProps.disabled === 'boolean') { + sharedProviderParams.formElement = { + disabled: nestedFieldProps.disabled, + } + } + if (formElement) { + sharedProviderParams.formElement = formElement + } + if (FormStatus) { + sharedProviderParams.FormStatus = FormStatus + } + if (locale) { + sharedProviderParams.locale = locale + } + sharedProviderParams.translations = useMemo(() => { + const translations = extendDeep( + {}, + sharedContext.translations, + restProps?.translations, + dataContextRef.current?.props?.translations + ) as ContextProps + + return translations + }, [restProps?.translations, sharedContext.translations]) + + const extend = useCallback( + (fieldProps: T) => { + // Extract props from data context to be used in fields + const { required: requiredByContext } = dataContextRef.current + + // Extract props from overwriteProps to be used in values + const key = overwriteProps && fieldProps?.path?.split('/')?.pop() + const overwrite = overwriteProps?.[key] + // Overwrite given schema props + if (overwrite && fieldProps?.schema) { + Object.keys(fieldProps.schema).forEach((key) => { + if (overwrite?.[key]) { + fieldProps.schema[key] = overwrite[key] + } + }) + } + + const props = overwrite + ? { ...fieldProps, ...overwrite } + : fieldProps + const required = + requiredByContext ?? nestedContext?.inheritedContext?.required + + const value = + typeof required !== 'undefined' || + Object.keys(nestedFieldProps).length > 0 + ? assignPropsWithContext( + props, + { required }, + nestedFieldProps as Record + ) + : props + + return value as T + }, + [ + nestedContext?.inheritedContext?.required, + nestedFieldProps, + overwriteProps, + ] + ) + + return { + extend, + inheritedProps: restProps, + inheritedContext: nestedFieldProps, + sharedProviderParams, + } +} + +export default useFieldProvider diff --git a/packages/dnb-eufemia/src/extensions/forms/Field/index.ts b/packages/dnb-eufemia/src/extensions/forms/Field/index.ts index 2190e5f122a..cbd10281a77 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Field/index.ts +++ b/packages/dnb-eufemia/src/extensions/forms/Field/index.ts @@ -1,3 +1,4 @@ +export { default as Provider } from './Provider' export { default as Composition } from './Composition' export { default as String } from './String' export { default as Number } from './Number' diff --git a/packages/dnb-eufemia/src/extensions/forms/Form/FieldProps/FieldProps.tsx b/packages/dnb-eufemia/src/extensions/forms/Form/FieldProps/FieldProps.tsx deleted file mode 100644 index 20bd6a6f7e2..00000000000 --- a/packages/dnb-eufemia/src/extensions/forms/Form/FieldProps/FieldProps.tsx +++ /dev/null @@ -1,141 +0,0 @@ -import React, { useCallback, useContext, useMemo, useRef } from 'react' -import DataContext, { ContextState } from '../../DataContext/Context' -import { Props as DataContextProps } from '../../DataContext/Provider' -import { FormStatusProps } from '../../../../components/FormStatus' -import { - assignPropsWithContext, - extendDeep, -} from '../../../../shared/component-helper' -import FieldPropsContext from './FieldPropsContext' -import SharedProvider from '../../../../shared/Provider' -import SharedContext, { ContextProps } from '../../../../shared/Context' -import type { FieldProps, Path, UseFieldProps } from '../../types' - -export type FieldPropsProps = FieldProps & { - children: React.ReactNode - - /** - * Locale to use for all nested Eufemia components - */ - locale?: DataContextProps['locale'] - - /** - * Provide your own translations. Use the same format as defined in the translation files - */ - translations?: DataContextProps['translations'] - - /** For internal use only */ - overwriteProps?: { - [key: Path]: FieldProps - } - - /** For internal use only */ - deep?: boolean - - /** For internal use only */ - formElement?: ContextProps['formElement'] - - /** For internal use only */ - FormStatus?: { globalStatus: FormStatusProps } -} - -function FieldPropsProvider(props: FieldPropsProps) { - const { - children, - formElement, - FormStatus, - overwriteProps, - deep, - ...restProps - } = props - - const sharedProviderParams: ContextProps = {} - const nestedContext = useContext(FieldPropsContext) - const sharedContext = useContext(SharedContext) - const dataContextRef = useRef() - dataContextRef.current = useContext(DataContext) - - /** - * Always use data context as the last source for localization - */ - const locale = dataContextRef.current?.props?.locale ?? restProps?.locale - - const nestedFieldProps = useMemo(() => { - return Object.assign( - nestedContext?.inheritedProps || {}, - restProps - ) as UseFieldProps - }, [nestedContext?.inheritedProps, restProps]) - - if (typeof nestedFieldProps.disabled === 'boolean') { - sharedProviderParams.formElement = { - disabled: nestedFieldProps.disabled, - } - } - if (formElement) { - sharedProviderParams.formElement = formElement - } - if (FormStatus) { - sharedProviderParams.FormStatus = FormStatus - } - if (locale) { - sharedProviderParams.locale = locale - } - sharedProviderParams.translations = useMemo(() => { - const translations = extendDeep( - {}, - sharedContext.translations, - restProps?.translations, - dataContextRef.current?.props?.translations - ) as ContextProps - - return translations - }, [restProps?.translations, sharedContext.translations]) - - const extend = useCallback( - (fieldProps: T) => { - // Extract props from data context to be used in fields - const { required: requiredByContext } = dataContextRef.current - - // Extract props from overwriteProps to be used in fields - const key = overwriteProps && fieldProps?.path?.split('/')?.pop() - const overwrite = overwriteProps?.[key] - - // Overwrite given schema props - if (overwrite && fieldProps?.schema) { - Object.keys(fieldProps.schema).forEach((key) => { - if (overwrite?.[key]) { - fieldProps.schema[key] = overwrite[key] - } - }) - } - - const value = assignPropsWithContext( - overwrite ? { ...fieldProps, ...overwrite } : fieldProps, - { - required: - requiredByContext ?? nestedContext?.inheritedContext?.required, - }, - nestedFieldProps as Record - ) - - return (deep ? nestedContext.extend(value) : value) as T - }, - [deep, nestedContext, nestedFieldProps, overwriteProps] - ) - - return ( - - {children} - - ) -} - -FieldPropsProvider._supportsSpacingProps = 'children' -export default FieldPropsProvider diff --git a/packages/dnb-eufemia/src/extensions/forms/Form/FieldProps/FieldPropsContext.ts b/packages/dnb-eufemia/src/extensions/forms/Form/FieldProps/FieldPropsContext.ts deleted file mode 100644 index f4d3db77ab7..00000000000 --- a/packages/dnb-eufemia/src/extensions/forms/Form/FieldProps/FieldPropsContext.ts +++ /dev/null @@ -1,15 +0,0 @@ -import React from 'react' -import { UseFieldProps } from '../../types' - -export type FieldPropsContextProps = { - extend: (props: T) => T - inheritedProps?: UseFieldProps - inheritedContext?: UseFieldProps -} - -const extend: FieldPropsContextProps['extend'] = (props) => props -const FieldPropsContext = React.createContext({ - extend, -}) - -export default FieldPropsContext diff --git a/packages/dnb-eufemia/src/extensions/forms/Form/FieldProps/index.ts b/packages/dnb-eufemia/src/extensions/forms/Form/FieldProps/index.ts deleted file mode 100644 index f04ba774dd0..00000000000 --- a/packages/dnb-eufemia/src/extensions/forms/Form/FieldProps/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from './FieldProps' -export * from './FieldProps' diff --git a/packages/dnb-eufemia/src/extensions/forms/Form/Section/Section.tsx b/packages/dnb-eufemia/src/extensions/forms/Form/Section/Section.tsx index 0701798869b..7712f998baf 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Form/Section/Section.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/Form/Section/Section.tsx @@ -2,7 +2,7 @@ import React, { useCallback, useContext, useMemo } from 'react' import SectionContext, { SectionContextState } from './SectionContext' import DataContext from '../../DataContext/Context' import Provider from '../../DataContext/Provider/Provider' -import FieldPropsProvider from '../FieldProps' +import FieldPropsProvider from '../../Field/Provider' import SectionContainerProvider from './containers/SectionContainerProvider' import ViewContainer from './ViewContainer' import EditContainer from './EditContainer' diff --git a/packages/dnb-eufemia/src/extensions/forms/Form/Section/__tests__/Section.test.tsx b/packages/dnb-eufemia/src/extensions/forms/Form/Section/__tests__/Section.test.tsx index 9f3127e185d..4c2992accd1 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Form/Section/__tests__/Section.test.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/Form/Section/__tests__/Section.test.tsx @@ -4,7 +4,7 @@ import { fireEvent, render } from '@testing-library/react' import { Field, Form, JSONSchema, Tools, Value } from '../../..' import { SectionProps } from '../Section' import { Props as FieldNameProps } from '../../../Field/Name' -import FieldPropsProvider from '../../FieldProps' +import FieldPropsProvider from '../../../Field/Provider' import { GenerateRef as GeneratePropsRef } from '../../../Tools/ListAllProps' import { GenerateRef as GenerateSchemaRef } from '../../../Tools/GenerateSchema' diff --git a/packages/dnb-eufemia/src/extensions/forms/Form/Visibility/Visibility.tsx b/packages/dnb-eufemia/src/extensions/forms/Form/Visibility/Visibility.tsx index f9b8cbca37e..244f103abf1 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Form/Visibility/Visibility.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/Form/Visibility/Visibility.tsx @@ -5,7 +5,7 @@ import useMountEffect from '../../../../shared/helpers/useMountEffect' import HeightAnimation, { HeightAnimationProps, } from '../../../../components/HeightAnimation' -import FieldProps from '../FieldProps' +import FieldProvider from '../../Field/Provider' import useVisibility from './useVisibility' import type { Path, UseFieldProps } from '../../types' @@ -142,7 +142,7 @@ function Visibility({ compensateForGap={compensateForGap} {...rest} > - {content} + {content} ) } @@ -151,7 +151,7 @@ function Visibility({ const props = !open ? fieldPropsWhenHidden : null return ( ) } diff --git a/packages/dnb-eufemia/src/extensions/forms/Form/index.ts b/packages/dnb-eufemia/src/extensions/forms/Form/index.ts index 97b605f8d96..ddb4dd9a3bb 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Form/index.ts +++ b/packages/dnb-eufemia/src/extensions/forms/Form/index.ts @@ -1,3 +1,5 @@ +import FieldProvider from '../Field/Provider/FieldProvider' + export { default as Handler } from './Handler' export { default as Element } from './Element' export { default as Appearance } from './Appearance' @@ -9,7 +11,6 @@ export { default as SubHeading } from './SubHeading' export { default as Visibility } from './Visibility' export { default as Section } from './Section' export { default as Isolation } from './Isolation' -export { default as FieldProps } from './FieldProps' export { default as useData } from './data-context/useData' export { default as setData } from './data-context/setData' export { default as getData } from './data-context/getData' @@ -28,3 +29,9 @@ export { default as useLocale } from '../hooks/useTranslation' * @deprecated Use `useValidation` instead */ export { default as useError } from './data-context/useValidation' + +/** + * Can be removed in v11 + * @deprecated Use `Field.Provider` instead + */ +export const FieldProps = FieldProvider diff --git a/packages/dnb-eufemia/src/extensions/forms/Wizard/Step/Step.tsx b/packages/dnb-eufemia/src/extensions/forms/Wizard/Step/Step.tsx index f3d7606853e..da8ce39c1fc 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Wizard/Step/Step.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/Wizard/Step/Step.tsx @@ -5,7 +5,7 @@ import { Props as FlexContainerProps } from '../../../../components/flex/Contain import WizardContext from '../Context/WizardContext' import Flex from '../../../../components/flex/Flex' import { convertJsxToString } from '../../../../shared/component-helper' -import FieldProps from '../../Form/FieldProps' +import FieldProvider from '../../Field/Provider' import type { VisibleWhen } from '../../Form/Visibility' // SSR warning fix: https://gist.github.com/gaearon/e7d97cdf38a2907924ea12e4ebdf3c85 @@ -107,7 +107,7 @@ function Step(props: Props): JSX.Element { {...restProps} > {fieldProps ? ( - {children} + {children} ) : ( children )} diff --git a/packages/dnb-eufemia/src/extensions/forms/hooks/useFieldProps.ts b/packages/dnb-eufemia/src/extensions/forms/hooks/useFieldProps.ts index 8ed7f045ece..db64ee9bf93 100644 --- a/packages/dnb-eufemia/src/extensions/forms/hooks/useFieldProps.ts +++ b/packages/dnb-eufemia/src/extensions/forms/hooks/useFieldProps.ts @@ -24,7 +24,7 @@ import { } from '../types' import { Context as DataContext, ContextState } from '../DataContext' import { clearedData } from '../DataContext/Provider/Provider' -import FieldPropsContext from '../Form/FieldProps/FieldPropsContext' +import FieldProviderContext from '../Field/Provider/FieldProviderContext' import { combineDescribedBy, warn } from '../../../shared/component-helper' import useId from '../../../shared/helpers/useId' import useUpdateEffect from '../../../shared/helpers/useUpdateEffect' @@ -84,7 +84,7 @@ export default function useFieldProps( localeProps: Props & FieldPropsGeneric, { executeOnChangeRegardlessOfError = false } = {} ): typeof localeProps & ReturnAdditional { - const { extend } = useContext(FieldPropsContext) + const { extend } = useContext(FieldProviderContext) const props = extend(localeProps) const {