diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/DataContext/Provider/Examples.tsx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/DataContext/Provider/Examples.tsx index 20e00c36cbc..7c6912b7a4c 100644 --- a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/DataContext/Provider/Examples.tsx +++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/DataContext/Provider/Examples.tsx @@ -8,7 +8,7 @@ import { } from '@dnb/eufemia/src/extensions/forms' import { Flex } from '@dnb/eufemia/src' -export const TestdataSchema: JSONSchema = { +export const TestDataSchema: JSONSchema = { type: 'object', properties: { requiredString: { type: 'string' }, @@ -37,7 +37,7 @@ export const TestdataSchema: JSONSchema = { required: ['requiredString'], } -export interface Testdata { +export type TestData = { requiredString: string string?: string number?: number @@ -53,7 +53,7 @@ export interface Testdata { }> } -export const testdata: Testdata = { +export const testData: TestData = { requiredString: 'This is a text', string: 'String value', number: 123, @@ -81,12 +81,12 @@ export const Default = () => { scope={{ DataContext, Value, - testdata, - TestdataSchema, + testData, + TestDataSchema, }} > console.log('onChange', data)} onPathChange={(path, value) => console.log('onPathChange', path, value) @@ -177,13 +177,13 @@ export const ValidationWithJsonSchema = () => { scope={{ DataContext, Value, - testdata, - TestdataSchema, + testData, + TestDataSchema, }} > console.log('onChange', data)} onPathChange={(path, value) => console.log('onPathChange', path, value) diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/Card/Examples.tsx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/Card/Examples.tsx index cdef9b83088..dc3cd285546 100644 --- a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/Card/Examples.tsx +++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/Card/Examples.tsx @@ -1,6 +1,11 @@ import ComponentBox from '../../../../../../shared/tags/ComponentBox' import { Flex, P } from '@dnb/eufemia/src' -import { Form, Field } from '@dnb/eufemia/src/extensions/forms' +import { + Form, + Field, + Wizard, + Value, +} from '@dnb/eufemia/src/extensions/forms' export const BasicUsage = () => { return ( @@ -19,3 +24,30 @@ export const BasicUsage = () => { ) } + +export const UsageInWizard = () => { + return ( + + + + + + + + + + + + + + + + + + + + ) +} diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/Card/demos.mdx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/Card/demos.mdx index c02baef433b..7d9f516c67e 100644 --- a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/Card/demos.mdx +++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/Card/demos.mdx @@ -8,3 +8,7 @@ import * as Examples from './Examples' ## Demos + + + + diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/Handler.mdx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/Handler.mdx index 5e322095c31..f19bea3c156 100644 --- a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/Handler.mdx +++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/Handler.mdx @@ -1,6 +1,6 @@ --- title: 'Handler' -description: '`Form.Handler` provides both the DataContext.Provider and a HTML form element.' +description: 'The `Form.Handler` is the root component of your form. It provides a HTML form element and handles the form data.' showTabs: true tabs: - title: Info diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/Handler/info.mdx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/Handler/info.mdx index 17715911b4b..8482711bd81 100644 --- a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/Handler/info.mdx +++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/Handler/info.mdx @@ -7,28 +7,126 @@ import AsyncChangeExample from './parts/async-change-example.mdx' ## Description -The `Form.Handler` component provides a HTML form element. +The `Form.Handler` is the root component of your form. It provides a HTML form element and handles the form data. + +```tsx +import { Form } from '@dnb/eufemia/extensions/forms' + +const existingData = { firstName: 'Nora' } + +function MyForm() { + return ( + + Your Form + + ) +} +``` + +### TypeScript support + +You can define the TypeScript type structure for your data like so: + +```tsx +import { Form } from '@dnb/eufemia/extensions/forms' + +type MyDataContext = { + firstName?: string +} + +// Method #1 – without initial data +function MyForm() { + return ( + + onSubmit={(data) => { + // "firstName" is of type string + console.log(data.firstName) + }} + > + ... + + ) +} + +// Method #2 – with data (initial values) +const existingData: MyDataContext = { + firstName: 'Nora', +} +function MyForm() { + return ( + { + // "firstName" is of type string + console.log(data.firstName) + }} + > + ... + + ) +} + +// Method #3 – type definition on the event parameter +const submitHandler = (data: MyDataContext) => { + // "firstName" is of type string + console.log(data.firstName) +} +function MyForm() { + return ... +} + +// Method #4 – type definition for the submit handler +import type { OnSubmit } from '@dnb/eufemia/extensions/forms' +const submitHandler: OnSubmit = (data) => { + // "firstName" is of type string + console.log(data.firstName) +} +function MyForm() { + return ... +} +``` + +To disable types you can: + +```tsx +>... +``` + +## Decoupling the form element + +For more flexibility, you can decouple the form element from the form context by using the `decoupleForm` property. It is recommended to use the `Form.Element` to wrap your rest of your form: ```jsx import { Form } from '@dnb/eufemia/extensions/forms' -render( - - Your Form - , -) +function MyApp() { + return ( + + + + Heading + + + + + + + + ) +} ``` -The form data can be handled outside of the form. This is useful if you want to use the form data in other components: +## Data handling + +You can access, mutate and filter data inside of the form context by using the `Form.useData` hook: ```jsx import { Form } from '@dnb/eufemia/extensions/forms' -function MyForm() { +function MyComponent() { const { getValue, update, @@ -37,9 +135,18 @@ function MyForm() { data, filterData, reduceToVisibleFields, - } = Form.useData('unique') + } = Form.useData() - return ... + return <>... +} + +function MyApp() { + return ( + <> + ... + + + ) } ``` @@ -51,53 +158,33 @@ function MyForm() { - `filterData` will filter the data based on your own logic. - `reduceToVisibleFields` will reduce the given data set to only contain the visible fields (mounted fields). -More examples can be found in the [useData](/uilib/extensions/forms/Form/useData/) hook docs. - -### TypeScript support +### Using a form ID -You can define the TypeScript type structure for your data like so: +The form data can be handled outside of the form. This is useful if you want to use the form data in other components: -```tsx +```jsx import { Form } from '@dnb/eufemia/extensions/forms' -type MyDataSet = { - firstName?: string -} +const myFormId = 'unique-id' // or a function, object or React Context reference -const data: MyDataSet = { - firstName: 'Nora', +function MyComponent() { + const { data } = Form.useData(myFormId) + + return <>... } -// Method #1 -function MyForm() { +function MyApp() { return ( - { - console.log(data.firstName) - }} - /> + <> + ... + + ) } - -// Method #2 -const submitHandler = (data: MyDataSet) => { - console.log(data.firstName) -} -function MyForm() { - return -} - -// Method #3 -import type { OnSubmit } from '@dnb/eufemia/extensions/forms' -const submitHandler: OnSubmit = (data) => { - console.log(data.firstName) -} -function MyForm() { - return -} ``` +More examples can be found in the [useData](/uilib/extensions/forms/Form/useData/) hook docs. + ## Async `onChange` and `onSubmit` event handlers **NB:** When using an async `onChange` event handler, the `data` parameter will only include validated data. This lets you utilize the `data` parameter directly in your request, without having to further process or transform it. diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/Handler/parts/async-state-return-example.mdx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/Handler/parts/async-state-return-example.mdx index faf06e5a976..52064fe781a 100644 --- a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/Handler/parts/async-state-return-example.mdx +++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/Handler/parts/async-state-return-example.mdx @@ -3,6 +3,8 @@ In all async operations, you can simply return an error object to display it in ```tsx import { Form } from '@dnb/eufemia/extensions/forms' +const myFormId = 'unique-id' // or a function, object or React Context reference + // Async function const onSubmit = async (data) => { try { @@ -12,7 +14,7 @@ const onSubmit = async (data) => { }) const data = await response.json() - Form.setData('unique-id', data) // Whatever you want to do with the data + Form.setData(myFormId, data) // Whatever you want to do with the data } catch (error) { return error // Will display the error message in the form } @@ -32,7 +34,7 @@ const onSubmit = async (data) => { function Component() { return ( - + ... ) diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/Isolation/Examples.tsx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/Isolation/Examples.tsx index f4f0547bb9b..8efe06bec36 100644 --- a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/Isolation/Examples.tsx +++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/Isolation/Examples.tsx @@ -66,14 +66,17 @@ export const CommitHandleRef = () => { { - const value = - isolatedData.newPerson.title.toLowerCase() + // Because of missing TypeScript support + const contactPersons = handlerData['contactPersons'] + const newPerson = isolatedData['newPerson'] + + const value = newPerson.title.toLowerCase() const transformedData = { ...handlerData, contactPersons: [ - ...handlerData.contactPersons, + ...contactPersons, { - ...isolatedData.newPerson, + ...newPerson, value, }, ], @@ -151,14 +154,18 @@ export const TransformCommitData = () => { { + // Because of missing TypeScript support + const contactPersons = + handlerData['contactPersons'] + const newPerson = isolatedData['newPerson'] + return { ...handlerData, contactPersons: [ - ...handlerData.contactPersons, + ...contactPersons, { - ...isolatedData.newPerson, - value: - isolatedData.newPerson.title.toLowerCase(), + ...newPerson, + value: newPerson.title.toLowerCase(), }, ], } diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/Isolation/info.mdx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/Isolation/info.mdx index d28b6bcd1d9..871d6bdae1b 100644 --- a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/Isolation/info.mdx +++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/Isolation/info.mdx @@ -42,6 +42,37 @@ export function MyForm(props) { } ``` +## TypeScript support + +You can define the TypeScript type structure for your data like so: + +```tsx +import { Form, Field } from '@dnb/eufemia/extensions/forms' + +type IsolationData = { + persons: Array<{ name: string }> + newPerson: Array<{ name: string }> +} + +function MyForm() { + return ( + + onCommit={(data) => { + data // <-- is of type IsolationData + }} + transformOnCommit={(isolatedData, handlerData) => { + return { + ...handlerData, + persons: [...handlerData.persons, isolatedData.newPerson], + } + }} + > + ... + + ) +} +``` + ## Commit the data to the form You can either use the `Form.Isolation.CommitButton` or provide a custom ref handler you can use (call) when you want to commit the data to the `Form.Handler` context: @@ -111,7 +142,7 @@ render( ## Clear data from isolated fields -You can clear the isolation by calling `Form.clearData` with the `id` of the form. +You can clear the isolation by calling `clearData`: ```jsx import { Form, Field } from '@dnb/eufemia/extensions/forms' @@ -120,9 +151,8 @@ function MyForm() { return ( { - Form.clearData('my-isolated-data') + onCommit={(data, { clearData }) => { + clearData() }} > diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/clearData/info.mdx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/clearData/info.mdx index 4c64d580f71..568a020e460 100644 --- a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/clearData/info.mdx +++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/clearData/info.mdx @@ -9,10 +9,12 @@ The `Form.clearData` lets you clear the data of a form. ```jsx import { Form } from '@dnb/eufemia/extensions/forms' +const myFormId = 'unique-id' // or a function, object or React Context reference + function Component() { - return ... + return ... } // You can call it later and even outside of the form -Form.clearData('unique-id') +Form.clearData(myFormId) ``` diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/getData/info.mdx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/getData/info.mdx index 9abadb531ef..1f63ccc92c3 100644 --- a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/getData/info.mdx +++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/getData/info.mdx @@ -9,13 +9,15 @@ With the `Form.getData` method, you can manage your form data outside of the for ```jsx import { Form } from '@dnb/eufemia/extensions/forms' +const myFormId = 'unique-id' // or a function, object or React Context reference + function Component() { - return ... + return ... } // Later, when there is data available const { getValue, data, filterData, reduceToVisibleFields } = - Form.getData('unique-id') + Form.getData(myFormId) ``` - `getValue` will return the value of the given path. @@ -23,7 +25,7 @@ const { getValue, data, filterData, reduceToVisibleFields } = - `filterData` will filter the data based on your own logic. - `reduceToVisibleFields` will reduce the given data set to only contain the visible fields (mounted fields). -You link them together via the `id` (string) property. +You link them together via the `id` (string, function, object or React Context as the reference) property. TypeScript support: @@ -44,9 +46,11 @@ You can use the `reduceToVisibleFields` function to get only the data of visible ```tsx import { Form } from '@dnb/eufemia/extensions/forms' +const myFormId = 'unique-id' // or a function, object or React Context reference + const MyForm = () => { return ( - + @@ -55,7 +59,7 @@ const MyForm = () => { } // Later, when there is data available -const { data, reduceToVisibleFields } = Form.getData('unique-id') +const { data, reduceToVisibleFields } = Form.getData(myFormId) const visibleData = reduceToVisibleFields(data) ``` @@ -77,9 +81,11 @@ The callback function should return a `boolean` or `undefined`. Return `false` t It returns the filtered form data. ```tsx +const myFormId = 'unique-id' // or a function, object or React Context reference + const MyForm = () => { return ( - + ) @@ -92,6 +98,6 @@ const filterDataHandler = ({ path, value, data, props, internal }) => { } // Later, when there is data available -const { filterData } = Form.getData('unique-id') +const { filterData } = Form.getData(myFormId) const filteredData = filterData(filterDataHandler) ``` diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/info.mdx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/info.mdx index ff06f7272f5..123e0cd6786 100644 --- a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/info.mdx +++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/info.mdx @@ -6,14 +6,30 @@ showTabs: true `Form` provides the main forms-helpers including data provider and event handling. -```jsx +```tsx import { Form, Field } from '@dnb/eufemia/extensions/forms' -render( - - - - - - , -) + +const existingData = { + email: 'name@email.no', +} + +function MyForm() { + return ( + { + await makeRequest(data) + }} + > + Heading + + + + + + + + + ) +} ``` diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/setData/info.mdx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/setData/info.mdx index eac652cd94e..e0563f8a15f 100644 --- a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/setData/info.mdx +++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/setData/info.mdx @@ -9,14 +9,16 @@ With the `Form.setData` method, you can manage your form data outside of the for ```jsx import { Form } from '@dnb/eufemia/extensions/forms' +const myFormId = 'unique-id' // or a function, object or React Context reference + Form.setData('unique', { foo: 'bar' }) function Component() { - return ... + return ... } ``` -You link them together via the `id` (string) property. +You link them together via the `id` (string, function, object or React Context as the reference) property. Related helpers: diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/useData/info.mdx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/useData/info.mdx index 34c96df04a6..19f6733aaba 100644 --- a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/useData/info.mdx +++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/useData/info.mdx @@ -42,7 +42,17 @@ render( ## Usage -You can use the `Form.useData` hook with or without an `id` (string) property, which is optional and can be used to link the data to a specific [Form.Handler](/uilib/extensions/forms/Form/Handler/) component. +You can use the `Form.useData` hook with or without an `id` (string, function, object or React Context as the reference) property, which is optional and can be used to link the data to a specific [Form.Handler](/uilib/extensions/forms/Form/Handler/) component. + +### TypeScript support + +You can define the TypeScript type structure for your data like so: + +```tsx +type Data = { foo: string } + +const { data } = Form.useData() +``` ### Without an `id` property @@ -66,36 +76,29 @@ function Component() { ### With an `id` property -While in this example, "Component" is outside the `Form.Handler` context, but linked together via the `id` (string) property: +While in this example, "Component" is outside the `Form.Handler` context, but linked together via the `id` (string, function, object or React Context as the reference) property: ```jsx import { Form } from '@dnb/eufemia/extensions/forms' +const myFormId = 'unique-id' // or a function, object or React Context reference + function MyForm() { return ( <> - ... + ... ) } function Component() { - const { data } = Form.useData('unique') + const { data } = Form.useData(myFormId) } ``` This is beneficial when you need to utilize the form data in other places within your application. -### TypeScript support - -You can define the TypeScript type structure for your data like so: - -```tsx -type Data = { foo: string } -const { data } = Form.useData('unique') -``` - ### Select a single value ```jsx @@ -136,15 +139,17 @@ With the `set` method, you can extend the data set. Existing data paths will be ```jsx import { Form, Field } from '@dnb/eufemia/extensions/forms' +const myFormId = 'unique-id' // or a function, object or React Context reference + function MyForm() { - const { data, set } = Form.useData('unique') + const { data, set } = Form.useData(myFormId) useEffect(() => { set({ foo: 'bar' }) }, []) return ( - + ) @@ -211,12 +216,14 @@ const filterDataHandler = ({ path, value, data, props, internal }) => { } } +const myFormId = 'unique-id' // or a function, object or React Context reference + const MyForm = () => { - const { filterData } = Form.useData('my-form') + const { filterData } = Form.useData(myFormId) const filteredData = filterData(filterDataHandler) return ( - + ) @@ -238,22 +245,23 @@ You decide where and when you want to provide the initial `data` to the form. It ```jsx import { Form, Field } from '@dnb/eufemia/extensions/forms' +const myFormId = 'unique-id' // or a function, object or React Context reference const initialData = { foo: 'bar' } function MyForm() { return ( - + ) } function ComponentA() { - Form.useData('unique', { foo: 'bar' }) + Form.useData(myFormId, { foo: 'bar' }) } function ComponentB() { - const { set } = Form.useData('unique') + const { set } = Form.useData(myFormId) useEffect(() => { set({ foo: 'bar' }) diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/useSnapshot/info.mdx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/useSnapshot/info.mdx index b4d6895f3c1..135684988bc 100644 --- a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/useSnapshot/info.mdx +++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/useSnapshot/info.mdx @@ -61,7 +61,7 @@ You can check out examples in the demo section. ## Usage of the `Form.useSnapshot` hook -You can use the `Form.useSnapshot` hook with or without an `id` (string) property, which is optional and can be used to link the data to a specific [Form.Handler](/uilib/extensions/forms/Form/Handler/) component. +You can use the `Form.useSnapshot` hook with or without an `id` (string, function, object or React Context as the reference) property, which is optional and can be used to link the data to a specific [Form.Handler](/uilib/extensions/forms/Form/Handler/) component. ### Without an `id` property @@ -85,22 +85,24 @@ function Component() { ### With an `id` property -While in this example, "Component" is outside the `Form.Handler` context, but linked together via the `id` (string) property: +While in this example, "Component" is outside the `Form.Handler` context, but linked together via the `id` (string, function, object or React Context as the reference) property: ```jsx import { Form } from '@dnb/eufemia/extensions/forms' +const myFormId = 'unique-id' // or a function, object or React Context reference + function MyForm() { return ( <> - ... + ... ) } function Component() { - const { createSnapshot, revertSnapshot } = Form.useSnapshot('unique') + const { createSnapshot, revertSnapshot } = Form.useSnapshot(myFormId) } ``` diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/useValidation/info.mdx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/useValidation/info.mdx index 3d74e108e06..b8a03449ef4 100644 --- a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/useValidation/info.mdx +++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/useValidation/info.mdx @@ -45,22 +45,24 @@ function Component() { } ``` -Or by linking the hook together with the form by using the `id` (string) property: +Or by linking the hook together with the form by using the `id` (string, function, object or React Context as the reference) property: ```jsx import { Form } from '@dnb/eufemia/extensions/forms' +const myFormId = 'unique-id' // or a function, object or React Context reference + function MyForm() { return ( <> - ... + ... ) } function Component() { - const { hasErrors, hasFieldError } = Form.useValidation('unique') + const { hasErrors, hasFieldError } = Form.useValidation(myFormId) } ``` @@ -69,10 +71,12 @@ Or by using it in the form component itself: ```jsx import { Form } from '@dnb/eufemia/extensions/forms' +const myFormId = 'unique-id' // or a function, object or React Context reference + function MyForm() { - const { hasErrors } = Form.useValidation('unique') + const { hasErrors } = Form.useValidation(myFormId) - return ... + return ... } ``` @@ -83,14 +87,16 @@ You can also report a form error that gets displayed on the bottom of the form b ```jsx import { Form } from '@dnb/eufemia/extensions/forms' +const myFormId = 'unique-id' // or a function, object or React Context reference + function MyForm() { - const { setFormError } = Form.useValidation('unique') + const { setFormError } = Form.useValidation(myFormId) useEffect(() => { setFormError('This is a global form error') }, []) - return ... + return ... } ``` @@ -101,12 +107,14 @@ You can also use the `setFieldStatus` method to report field status. This will u ```jsx import { Form, Field } from '@dnb/eufemia/extensions/forms' +const myFormId = 'unique-id' // or a function, object or React Context reference + function Component() { - const { setFieldStatus } = Form.useValidation('unique') + const { setFieldStatus } = Form.useValidation(myFormId) return ( { // Report a field status setFieldStatus('/path/to/field', { diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Iterate/Array/Examples.tsx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Iterate/Array/Examples.tsx index 1c947014598..a238bd87094 100644 --- a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Iterate/Array/Examples.tsx +++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Iterate/Array/Examples.tsx @@ -1,5 +1,5 @@ import ComponentBox from '../../../../../../shared/tags/ComponentBox' -import { Flex, Table, Td, Th, Tr } from '@dnb/eufemia/src' +import { Avatar, Flex, Table, Td, Th, Tr } from '@dnb/eufemia/src' import { Iterate, Field, @@ -205,7 +205,7 @@ export const ArrayFromFormHandler = () => { - + @@ -549,7 +549,7 @@ export const WithArrayValidator = () => { width="medium" size="medium" /> - + @@ -565,3 +565,138 @@ export const WithArrayValidator = () => { ) } + +export const FilledViewAndEditContainer = () => { + return ( + + {() => { + const MyEditItemForm = () => { + return ( + + + + + ) + } + + const EditItemToolbar = () => { + return ( + + + + + + + + + + ) + } + + const MyEditItem = (props) => { + return ( + } + {...props} + > + + + + ) + } + + const CreateNewEntry = () => { + return ( + + } + showOpenButtonWhen={(list) => list.length > 0} + > + + + ) + } + + const ValueWithAvatar = () => { + const { value } = Iterate.useItem() + const firstName = String(value['firstName'] || '') + return ( + + + {firstName.substring(0, 1).toUpperCase()} + + + + ) + } + + const MyViewItem = () => { + return ( + } + > + + + + + + + + + ) + } + + return ( + console.log('onSubmit', data)} + onSubmitRequest={() => console.log('onSubmitRequest')} + > + + Accounts + + + + + + + + + + + + + ) + }} + + ) +} diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Iterate/Array/demos.mdx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Iterate/Array/demos.mdx index 7b8e20894be..42d6de4bd4c 100644 --- a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Iterate/Array/demos.mdx +++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Iterate/Array/demos.mdx @@ -49,6 +49,13 @@ With an optional `title` and [Iterate.Toolbar](/uilib/extensions/forms/Iterate/T +### Customize the view and edit containers + +- Using `variant="filled"` will render the [ViewContainer](/uilib/extensions/forms/Iterate/ViewContainer) and [EditContainer](/uilib/extensions/forms/Iterate/EditContainer) with a background color. +- Using `toolbarVariant="custom"` will render the [Toolbar](/uilib/extensions/forms/Iterate/Toolbar/) without any spacing so you can customize it to your needs. + + + ### Initially open diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Iterate/Count/info.mdx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Iterate/Count/info.mdx index 5d8d3007223..9fdb0ec5d5a 100644 --- a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Iterate/Count/info.mdx +++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Iterate/Count/info.mdx @@ -77,11 +77,13 @@ render( And call it as a function as well (id is required): ```tsx +const myFormId = 'unique-id' // or a function, object or React Context reference + function MyForm() { - const count = Iterate.count({ id: 'myForm', path: '/myList' }) + const count = Iterate.count({ id: myFormId, path: '/myList' }) return ( - + ) diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Iterate/EditContainer/info.mdx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Iterate/EditContainer/info.mdx index 9dc74e1820f..d761fac1342 100644 --- a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Iterate/EditContainer/info.mdx +++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Iterate/EditContainer/info.mdx @@ -55,8 +55,9 @@ You can get the internal item object by using the `Iterate.useItem` hook. import { Iterate, Field, Value } from '@dnb/eufemia/extensions/forms' const MyItemForm = () => { - const item = Iterate.useItem() - console.log('index:', item.index) + // TypeScript type inference + const item = Iterate.useItem<{ foo: string }>() + console.log('My item:', item.index, item.value.foo) return } diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Iterate/PushContainer/Examples.tsx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Iterate/PushContainer/Examples.tsx index 6485fe45460..f9d8d5151ec 100644 --- a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Iterate/PushContainer/Examples.tsx +++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Iterate/PushContainer/Examples.tsx @@ -143,7 +143,7 @@ export const IsolatedData = () => { function ExistingPersonDetails() { const { data, getValue } = Form.useData() - const person = getValue(data.selectedPerson)?.data || {} + const person = getValue(data['selectedPerson'])?.data || {} return ( @@ -172,14 +172,15 @@ export const IsolatedData = () => { function PushContainerContent() { const { data, update } = Form.useData() + const selectedPerson = data['selectedPerson'] // Because of missing TypeScript support // Clear the PushContainer data when the selected person is "other", // so the fields do not inherit existing data. React.useLayoutEffect(() => { - if (data.selectedPerson === 'other') { + if (selectedPerson === 'other') { update('/pushContainerItems/0', {}) } - }, [data.selectedPerson, update]) + }, [selectedPerson, update]) return ( diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Iterate/RemoveButton/info.mdx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Iterate/RemoveButton/info.mdx index 457cae6b65b..b38daeab302 100644 --- a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Iterate/RemoveButton/info.mdx +++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Iterate/RemoveButton/info.mdx @@ -20,3 +20,11 @@ render( , ) ``` + +## Confirm removal + +You can use the `showConfirmDialog` property to open a confirmation dialog before removing the item. + +```tsx + +``` diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Value/PhoneNumber/Examples.tsx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Value/PhoneNumber/Examples.tsx index 34b2c4e3d04..5de51301893 100644 --- a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Value/PhoneNumber/Examples.tsx +++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Value/PhoneNumber/Examples.tsx @@ -1,5 +1,5 @@ import ComponentBox from '../../../../../../shared/tags/ComponentBox' -import { P } from '@dnb/eufemia/src' +import { Flex, P } from '@dnb/eufemia/src' import { Value } from '@dnb/eufemia/src/extensions/forms' export const Empty = () => { @@ -45,9 +45,11 @@ export const LabelAndValue = () => { export const InternationalSuffix = () => { return ( - - - + + + + + ) } diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Wizard/Container/info.mdx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Wizard/Container/info.mdx index 89729d6ded8..31c7a7ef92c 100644 --- a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Wizard/Container/info.mdx +++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Wizard/Container/info.mdx @@ -117,17 +117,19 @@ const MyForm = () => { } ``` -When using the `useStep` hook outside of the `Wizard.Container` context, you need to provide an unique `id` (string): +When using the `useStep` hook outside of the `Wizard.Container` context, you need to provide an unique `id` (string, function, object or React Context as the reference): ```tsx import { Form, Wizard } from '@dnb/eufemia/extensions/forms' +const myContainerId = 'unique-id' // or a function, object or React Context reference + const MyForm = () => { - const { setActiveIndex, activeIndex } = Wizard.useStep('unique-id') + const { setActiveIndex, activeIndex } = Wizard.useStep(myContainerId) return ( - + diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Wizard/location-hooks/info.mdx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Wizard/location-hooks/info.mdx index 12512df0d29..40e692bffe9 100644 --- a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Wizard/location-hooks/info.mdx +++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Wizard/location-hooks/info.mdx @@ -106,7 +106,7 @@ function MyForm() { ## Without a router -You connect the hook with the `Wizard.Container` component via an unique `id` (string). The `id` will be used in the URL query string: `url?unique-id-step=1`. +You connect the hook with the `Wizard.Container` component via an unique `id` (string, function, object or React Context as the reference). The `id` will be used in the URL query string: `url?unique-id-step=1`. ```jsx import { Form, Wizard } from '@dnb/eufemia/extensions/forms' diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Wizard/useStep/info.mdx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Wizard/useStep/info.mdx index 0b743c3b7ac..99e1f925014 100644 --- a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Wizard/useStep/info.mdx +++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Wizard/useStep/info.mdx @@ -26,20 +26,22 @@ function MyForm() { } ``` -You can also connect the hook with the `Wizard.Container` via an `id` (string). This lets you render the hook outside of the context: +You can also connect the hook with the `Wizard.Container` via an `id` (string, function, object or React Context as the reference). This lets you render the hook outside of the context: ```jsx import { Form } from '@dnb/eufemia/extensions/forms' +const myContainerId = 'unique-id' // or a function, object or React Context reference + function Sidecar() { - const { activeIndex, setActiveIndex } = Wizard.useStep('unique-id') + const { activeIndex, setActiveIndex } = Wizard.useStep(myContainerId) } function MyForm() { return ( - ... + ... ) } diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/all-features.mdx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/all-features.mdx index 81241caf89d..cf0082c4d5f 100644 --- a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/all-features.mdx +++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/all-features.mdx @@ -56,7 +56,7 @@ const submitHandler = async (data) => { function Component() { return ( - + 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 7e88677912b..f2184166ec1 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 @@ -13,6 +13,18 @@ breadcrumb: Change log for the Eufemia Forms extension. +## v10.58 + +- Added `variant="filled"` to [Iterate.ViewContainer](/uilib/extensions/forms/Iterate/ViewContainer/) and [Iterate.EditContainer](/uilib/extensions/forms/Iterate/EditContainer/), to render with a background color. +- Added `toolbarVariant="custom"` to [Iterate.ViewContainer](/uilib/extensions/forms/Iterate/ViewContainer/) and [Iterate.EditContainer](/uilib/extensions/forms/Iterate/EditContainer/), to render the given toolbar without any spacing so it can be customized to your needs. +- Added `showConfirmDialog` to [Iterate.RemoveButton](/uilib/extensions/forms/Iterate/RemoveButton/), to open a confirmation dialog before removing the item. +- Added `decoupleForm` to [Form.Handler](/uilib/extensions/forms/Form/Handler/), to be able to use the data context in a more flexible way. +- Added support for using function reference instead of a string based `id` in [Form.Handler](/uilib/extensions/forms/Form/Handler/). +- Added `sessionStorageId` support to [Field.Upload](/uilib/extensions/forms/feature-fields/more-fields/Upload/) with empty file list rendering. +- Added [docs on how to deal with TypeScript types](/uilib/extensions/forms/getting-started/#typescript-support), and enhanced typings. +- Fixed so there is no outline when using `variant="basic"` in [Form.Section](/uilib/extensions/forms/Form/Section/) containers when used in [Wizard](/uilib/extensions/forms/Wizard/). +- Fixed formatting of country prefixes in [Value.PhoneNumber](/uilib/extensions/forms/Value/PhoneNumber/). + ## v10.57 - Added possibility for disabling individual options in [Field.Selection](/uilib/extensions/forms/base-fields/Selection/) and [Field.ArraySelection](/uilib/extensions/forms/base-fields/ArraySelection/). 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 8378af5c301..a90124edf63 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 @@ -20,6 +20,7 @@ import AsyncChangeExample from './Form/Handler/parts/async-change-example.mdx' - [Getting started](#getting-started) - [Creating forms](#creating-forms) + - [TypeScript support](#typescript-support) - [State management](#state-management) - [What is a JSON Pointer?](#what-is-a-json-pointer) - [Data handling](#data-handling) @@ -62,6 +63,37 @@ To build an entire form, there are surrounding components such as form [Handler] The needed styles are included in the Eufemia core package via `dnb-ui-components`. +### TypeScript support + +You can define the TypeScript type structure for your data like so: + +```tsx +import { Form } from '@dnb/eufemia/extensions/forms' + +type MyDataContext = { + firstName?: string +} + +function MyForm() { + return ( + + onSubmit={(data) => { + console.log(data.firstName) // "firstName" is of type string + }} + > + + + ) +} + +const MyComponent = () => { + const { data } = Form.useData() + return data.firstName +} +``` + +Read more about TypeScript support and the other methods in the [Form.Handler](/uilib/extensions/forms/Form/Handler/#typescript-support) section or in the [Form.useData](/uilib/extensions/forms/Form/useData/#typescript-support) hook docs. + ### State management While you can use a controlled method of handling the sate of fields or your form, it is recommended to use a declarative approach, where you keep the state of your form inside the data context, instead of pulling it out via your own `useState` hooks (imperative). @@ -121,16 +153,18 @@ How do I handle complex data logic? You can show or hide parts of your form based on your own logic. This is done by using the [Visibility](/uilib/extensions/forms/Form/Visibility/) component (yes, it even can animate the visibility). -And you can access and modify your form data with [useData](/uilib/extensions/forms/Form/useData/) or [getData](/uilib/extensions/forms/Form/getData/) or even [setData](/uilib/extensions/forms/Form/setData/). +And you can access and modify your form data with [Form.useData](/uilib/extensions/forms/Form/useData/) or [getData](/uilib/extensions/forms/Form/getData/) or even [setData](/uilib/extensions/forms/Form/setData/). Here is an example of how to use these methods: ```jsx import { Form } from '@dnb/eufemia/extensions/forms' +const myFormId = 'unique-id' // or a function, object or React Context reference + function MyForm() { return ( - + ) @@ -145,15 +179,15 @@ function MyComponent() { data, filterData, reduceToVisibleFields, - } = Form.useData() // optionally provide an id (unique-id) + } = Form.useData() // optionally provide an id or reference } // You can also use the setData: -Form.setData('unique-id', { companyName: 'DNB' }) +Form.setData(myFormId, { companyName: 'DNB' }) // ... and the getData – method when ever you need to: const { getValue, data, filterData, reduceToVisibleFields } = - Form.getData('unique-id') + Form.getData(myFormId) ``` - `getValue` will return the value of the given path. @@ -164,7 +198,7 @@ const { getValue, data, filterData, reduceToVisibleFields } = - `filterData` will filter the data based on your own logic. - `reduceToVisibleFields` will reduce the given data set to only contain the visible fields (mounted fields). -As you can see in the code above, you can even handle the state outside the `Form.Handler` context. You find more details on this topic in the [useData](/uilib/extensions/forms/Form/useData/) documentation. +As you can see in the code above, you can even handle the state outside the `Form.Handler` context. You find more details on this topic in the [Form.useData](/uilib/extensions/forms/Form/useData/) documentation. @@ -181,9 +215,9 @@ When submitting data to the server, you might want to exclude data that has been In this section we will show how to filter out some data based on your own logic. You can filter data by any given criteria. This is done by utilizing the `filterData` method from e.g.: -- [useData](/uilib/extensions/forms/Form/useData/#filter-data) hook. -- [getData](/uilib/extensions/forms/Form/getData/#filter-data) method. -- [Visibility](/uilib/extensions/forms/Form/Visibility/#filter-data) component. +- [Form.useData](/uilib/extensions/forms/Form/useData/#filter-data) hook. +- [Form.getData](/uilib/extensions/forms/Form/getData/#filter-data) method. +- [Form.Visibility](/uilib/extensions/forms/Form/Visibility/#filter-data) component. You can provide either a function handler or an object with the paths (JSON Pointer) you want to filter out. @@ -229,16 +263,16 @@ You may check out an [interactive example](/uilib/extensions/forms/Form/useData/ For filtering data during form submit (`onSubmit`), you can use the `filterData` method given as a parameter to the `onSubmit` event callback: ```tsx -const onSubmit = (data, { filterData }) => { - // Same method as in the previous example - const filteredDataA = filterData(filterDataPaths) - const filteredDataB = filterData(filterDataHandler) - console.log(filteredDataA) - console.log(filteredDataB) -} - render( - + { + // Same method as in the previous example + const filteredDataA = filterData(filterDataPaths) + const filteredDataB = filterData(filterDataHandler) + console.log(filteredDataA) + console.log(filteredDataB) + }} + > , ) @@ -426,7 +460,7 @@ Eufemia Forms will easily link up with the [GlobalStatus](/uilib/components/glob ```tsx - + My Form ``` diff --git a/packages/dnb-design-system-portal/src/shared/menu/SidebarMenu.module.scss b/packages/dnb-design-system-portal/src/shared/menu/SidebarMenu.module.scss index 8a9c19a8994..af09e235248 100644 --- a/packages/dnb-design-system-portal/src/shared/menu/SidebarMenu.module.scss +++ b/packages/dnb-design-system-portal/src/shared/menu/SidebarMenu.module.scss @@ -234,7 +234,7 @@ &__expand-button { align-self: center; - margin-right: 0.5rem; + margin-right: 1rem; &:hover { background-color: white; outline: 1px solid currentcolor; @@ -255,7 +255,7 @@ } > .dnb-sidebar-menu__item .dnb-sidebar-menu__expand-button { - margin-right: 0.25rem; + margin-right: 0.75rem; } } } diff --git a/packages/dnb-design-system-portal/src/shared/menu/SidebarMenu.tsx b/packages/dnb-design-system-portal/src/shared/menu/SidebarMenu.tsx index 4e9b32e1cc5..abb8f855952 100644 --- a/packages/dnb-design-system-portal/src/shared/menu/SidebarMenu.tsx +++ b/packages/dnb-design-system-portal/src/shared/menu/SidebarMenu.tsx @@ -417,6 +417,7 @@ function ListItem({ {hasSubheadings && ( { @@ -433,7 +434,6 @@ function ListItem({ )} - {/* Currently not nesting list items with an
    inside
  • as it breaks the styling for the time being */} ) } diff --git a/packages/dnb-eufemia/src/components/date-picker/DatePickerContext.ts b/packages/dnb-eufemia/src/components/date-picker/DatePickerContext.ts index 71b58afaede..3c0511fb275 100644 --- a/packages/dnb-eufemia/src/components/date-picker/DatePickerContext.ts +++ b/packages/dnb-eufemia/src/components/date-picker/DatePickerContext.ts @@ -23,7 +23,7 @@ export type DatePickerContextValues = ContextProps & translation: ContextProps['translation'] views: Array hasHadValidDate: boolean - previousDates: DatePickerDateProps + previousDateProps: DatePickerDateProps updateDates: ( dates: DatePickerDates, callback?: (dates: DatePickerDates) => void diff --git a/packages/dnb-eufemia/src/components/date-picker/DatePickerFooter.tsx b/packages/dnb-eufemia/src/components/date-picker/DatePickerFooter.tsx index c0f30ab8c2b..bf013bbf0ec 100644 --- a/packages/dnb-eufemia/src/components/date-picker/DatePickerFooter.tsx +++ b/packages/dnb-eufemia/src/components/date-picker/DatePickerFooter.tsx @@ -39,7 +39,7 @@ function DatePickerFooter({ }: DatePickerFooterProps) { const { updateDates, - previousDates, + previousDateProps, props: contextProps, } = useContext(DatePickerContext) @@ -69,18 +69,18 @@ function DatePickerFooter({ args.event.persist() } - const startDate = previousDates.startDate - ? convertStringToDate(previousDates.startDate, { + const startDate = previousDateProps.startDate + ? convertStringToDate(previousDateProps.startDate, { dateFormat, }) - : previousDates.date - ? convertStringToDate(previousDates.date, { + : previousDateProps.date + ? convertStringToDate(previousDateProps.date, { dateFormat, }) : null - const endDate = previousDates.endDate - ? convertStringToDate(previousDates.endDate, { + const endDate = previousDateProps.endDate + ? convertStringToDate(previousDateProps.endDate, { dateFormat, }) : startDate @@ -95,7 +95,7 @@ function DatePickerFooter({ } ) }, - [dateFormat, updateDates, previousDates, onCancel] + [dateFormat, updateDates, previousDateProps, onCancel] ) const onResetHandler = useCallback( diff --git a/packages/dnb-eufemia/src/components/date-picker/DatePickerProvider.tsx b/packages/dnb-eufemia/src/components/date-picker/DatePickerProvider.tsx index 52ff66a306e..2075d9cf611 100644 --- a/packages/dnb-eufemia/src/components/date-picker/DatePickerProvider.tsx +++ b/packages/dnb-eufemia/src/components/date-picker/DatePickerProvider.tsx @@ -91,22 +91,23 @@ function DatePickerProvider(externalProps: DatePickerProviderProps) { const sharedContext = useContext(SharedContext) - const { dates, updateDates, hasHadValidDate, previousDates } = useDates( - { - date, - startDate, - endDate, - startMonth, - endMonth, - minDate, - maxDate, - }, - { - dateFormat: dateFormat, - isRange: range, - shouldCorrectDate: correctInvalidDate, - } - ) + const { dates, updateDates, hasHadValidDate, previousDateProps } = + useDates( + { + date, + startDate, + endDate, + startMonth, + endMonth, + minDate, + maxDate, + }, + { + dateFormat: dateFormat, + isRange: range, + shouldCorrectDate: correctInvalidDate, + } + ) const { views, setViews, forceViewMonthChange } = useViews({ startMonth: dates.startMonth, @@ -225,7 +226,7 @@ function DatePickerProvider(externalProps: DatePickerProviderProps) { hidePicker: hidePicker, props, ...dates, - previousDates, + previousDateProps, hasHadValidDate, views, setViews, diff --git a/packages/dnb-eufemia/src/components/date-picker/__tests__/DatePicker.test.tsx b/packages/dnb-eufemia/src/components/date-picker/__tests__/DatePicker.test.tsx index 91091144f3d..65baae0bbc7 100644 --- a/packages/dnb-eufemia/src/components/date-picker/__tests__/DatePicker.test.tsx +++ b/packages/dnb-eufemia/src/components/date-picker/__tests__/DatePicker.test.tsx @@ -3,7 +3,7 @@ * */ -import React from 'react' +import React, { useState } from 'react' import { axeComponent, loadScss, wait } from '../../../core/jest/jestSetup' import userEvent from '@testing-library/user-event' import DatePicker, { DatePickerAllProps } from '../DatePicker' @@ -223,6 +223,264 @@ describe('DatePicker component', () => { ).not.toContain('dnb-date-picker--opened') }) + it('should delete input content one number at a time when `date` is "prop controlled"', async () => { + const Component = () => { + const [date, setDate] = useState('2024-05-17') + + return ( + setDate(date)} + /> + ) + } + + render() + + const [day, month, year]: Array = Array.from( + document.querySelectorAll('input.dnb-input__input') + ) + + expect(day.value).toBe('17') + expect(month.value).toBe('05') + expect(year.value).toBe('2024') + + await userEvent.click(year) + await userEvent.keyboard('{ArrowRight>4}{Backspace}') + + expect(day.value).toBe('17') + expect(month.value).toBe('05') + expect(year.value).toBe('202å') + + await userEvent.keyboard('{Backspace}') + + expect(day.value).toBe('17') + expect(month.value).toBe('05') + expect(year.value).toBe('20åå') + + await userEvent.keyboard('{Backspace}') + + expect(day.value).toBe('17') + expect(month.value).toBe('05') + expect(year.value).toBe('2ååå') + + await userEvent.keyboard('{Backspace}') + + expect(day.value).toBe('17') + expect(month.value).toBe('05') + expect(year.value).toBe('åååå') + + await userEvent.keyboard('{Backspace>2}') + + expect(day.value).toBe('17') + expect(month.value).toBe('0m') + expect(year.value).toBe('åååå') + + await userEvent.keyboard('{Backspace}') + + expect(day.value).toBe('17') + expect(month.value).toBe('mm') + expect(year.value).toBe('åååå') + + await userEvent.keyboard('{Backspace>2}') + + expect(day.value).toBe('1d') + expect(month.value).toBe('mm') + expect(year.value).toBe('åååå') + + await userEvent.keyboard('{Backspace}') + + expect(day.value).toBe('dd') + expect(month.value).toBe('mm') + expect(year.value).toBe('åååå') + }) + + it('should delete input content one number at a time when `startDate` and `endDate` is "prop controlled" and in ranged mode', async () => { + const Component = () => { + const [startDate, setStartDate] = useState('2024-05-01') + const [endDate, setEndDate] = useState('2025-06-30') + + return ( + { + setStartDate(start_date) + setEndDate(end_date) + }} + /> + ) + } + + render() + + const [ + startDay, + startMonth, + startYear, + endDay, + endMonth, + endYear, + ]: Array = Array.from( + document.querySelectorAll('input.dnb-input__input') + ) + + expect(startDay.value).toBe('01') + expect(startMonth.value).toBe('05') + expect(startYear.value).toBe('2024') + expect(endDay.value).toBe('30') + expect(endMonth.value).toBe('06') + expect(endYear.value).toBe('2025') + + await userEvent.click(endYear) + await userEvent.keyboard('{ArrowRight>4}{Backspace}') + + expect(startDay.value).toBe('01') + expect(startMonth.value).toBe('05') + expect(startYear.value).toBe('2024') + expect(endDay.value).toBe('30') + expect(endMonth.value).toBe('06') + expect(endYear.value).toBe('202å') + + await userEvent.keyboard('{Backspace}') + + expect(startDay.value).toBe('01') + expect(startMonth.value).toBe('05') + expect(startYear.value).toBe('2024') + expect(endDay.value).toBe('30') + expect(endMonth.value).toBe('06') + expect(endYear.value).toBe('20åå') + + await userEvent.keyboard('{Backspace}') + + expect(startDay.value).toBe('01') + expect(startMonth.value).toBe('05') + expect(startYear.value).toBe('2024') + expect(endDay.value).toBe('30') + expect(endMonth.value).toBe('06') + expect(endYear.value).toBe('2ååå') + + await userEvent.keyboard('{Backspace}') + + expect(startDay.value).toBe('01') + expect(startMonth.value).toBe('05') + expect(startYear.value).toBe('2024') + expect(endDay.value).toBe('30') + expect(endMonth.value).toBe('06') + expect(endYear.value).toBe('åååå') + + await userEvent.keyboard('{Backspace>2}') + + expect(startDay.value).toBe('01') + expect(startMonth.value).toBe('05') + expect(startYear.value).toBe('2024') + expect(endDay.value).toBe('30') + expect(endMonth.value).toBe('0m') + expect(endYear.value).toBe('åååå') + + await userEvent.keyboard('{Backspace}') + + expect(startDay.value).toBe('01') + expect(startMonth.value).toBe('05') + expect(startYear.value).toBe('2024') + expect(endDay.value).toBe('30') + expect(endMonth.value).toBe('mm') + expect(endYear.value).toBe('åååå') + + await userEvent.keyboard('{Backspace>2}') + + expect(startDay.value).toBe('01') + expect(startMonth.value).toBe('05') + expect(startYear.value).toBe('2024') + expect(endDay.value).toBe('3d') + expect(endMonth.value).toBe('mm') + expect(endYear.value).toBe('åååå') + + await userEvent.keyboard('{Backspace}') + + expect(startDay.value).toBe('01') + expect(startMonth.value).toBe('05') + expect(startYear.value).toBe('2024') + expect(endDay.value).toBe('dd') + expect(endMonth.value).toBe('mm') + expect(endYear.value).toBe('åååå') + + await userEvent.keyboard('{Backspace>2}') + + expect(startDay.value).toBe('01') + expect(startMonth.value).toBe('05') + expect(startYear.value).toBe('202å') + expect(endDay.value).toBe('dd') + expect(endMonth.value).toBe('mm') + expect(endYear.value).toBe('åååå') + + await userEvent.keyboard('{Backspace}') + + expect(startDay.value).toBe('01') + expect(startMonth.value).toBe('05') + expect(startYear.value).toBe('20åå') + expect(endDay.value).toBe('dd') + expect(endMonth.value).toBe('mm') + expect(endYear.value).toBe('åååå') + + await userEvent.keyboard('{Backspace}') + + expect(startDay.value).toBe('01') + expect(startMonth.value).toBe('05') + expect(startYear.value).toBe('2ååå') + expect(endDay.value).toBe('dd') + expect(endMonth.value).toBe('mm') + expect(endYear.value).toBe('åååå') + + await userEvent.keyboard('{Backspace}') + + expect(startDay.value).toBe('01') + expect(startMonth.value).toBe('05') + expect(startYear.value).toBe('åååå') + expect(endDay.value).toBe('dd') + expect(endMonth.value).toBe('mm') + expect(endYear.value).toBe('åååå') + + await userEvent.keyboard('{Backspace>2}') + + expect(startDay.value).toBe('01') + expect(startMonth.value).toBe('0m') + expect(startYear.value).toBe('åååå') + expect(endDay.value).toBe('dd') + expect(endMonth.value).toBe('mm') + expect(endYear.value).toBe('åååå') + + await userEvent.keyboard('{Backspace}') + + expect(startDay.value).toBe('01') + expect(startMonth.value).toBe('mm') + expect(startYear.value).toBe('åååå') + expect(endDay.value).toBe('dd') + expect(endMonth.value).toBe('mm') + expect(endYear.value).toBe('åååå') + + await userEvent.keyboard('{Backspace>2}') + + expect(startDay.value).toBe('0d') + expect(startMonth.value).toBe('mm') + expect(startYear.value).toBe('åååå') + expect(endDay.value).toBe('dd') + expect(endMonth.value).toBe('mm') + expect(endYear.value).toBe('åååå') + + await userEvent.keyboard('{Backspace}') + + expect(startDay.value).toBe('dd') + expect(startMonth.value).toBe('mm') + expect(startYear.value).toBe('åååå') + expect(endDay.value).toBe('dd') + expect(endMonth.value).toBe('mm') + expect(endYear.value).toBe('åååå') + }) + it('will render the result of "onDaysRender"', () => { const customClassName = 'dnb-date-picker__day--weekend' const onDaysRender = jest.fn((days) => { @@ -2315,51 +2573,6 @@ describe('DatePicker calc', () => { rightPicker.querySelector('.dnb-date-picker__header__title') ).toHaveTextContent('november 2024') }) - - it('should remove end date from ranged input, where dates are prop controlled, when pressing backspace', async () => { - const Component = () => { - const [startDate, setStartDate] = React.useState('2024-10-10') - const [endDate, setEndDate] = React.useState('2024-11-21') - - return ( - { - setStartDate(start_date) - setEndDate(end_date) - }} - /> - ) - } - - render() - - const [startDay, startMonth, startYear, endDay, endMonth, endYear] = - Array.from( - document.querySelectorAll('.dnb-date-picker__input') - ) as Array - - expect(startDay.value).toBe('10') - expect(startMonth.value).toBe('10') - expect(startYear.value).toBe('2024') - expect(endDay.value).toBe('21') - expect(endMonth.value).toBe('11') - expect(endYear.value).toBe('2024') - - await userEvent.click(endYear) - await userEvent.keyboard('{backspace>3}') - - expect(startDay.value).toBe('10') - expect(startMonth.value).toBe('10') - expect(startYear.value).toBe('2024') - expect(endDay.value).toBe('dd') - expect(endMonth.value).toBe('mm') - expect(endYear.value).toBe('åååå') - }) }) describe('DatePicker scss', () => { diff --git a/packages/dnb-eufemia/src/components/date-picker/hooks/useDates.ts b/packages/dnb-eufemia/src/components/date-picker/hooks/useDates.ts index 15368647a06..ee2433cfcd7 100644 --- a/packages/dnb-eufemia/src/components/date-picker/hooks/useDates.ts +++ b/packages/dnb-eufemia/src/components/date-picker/hooks/useDates.ts @@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { convertStringToDate, isDisabled } from '../DatePickerCalc' import isValid from 'date-fns/isValid' import format from 'date-fns/format' -import { addMonths } from 'date-fns' +import { addMonths, isSameDay } from 'date-fns' import { DateType } from '../DatePickerContext' export type DatePickerDateProps = { @@ -53,9 +53,9 @@ export default function useDates( shouldCorrectDate = false, }: UseDatesOptions ) { - const [previousDates, setPreviousDates] = useState(dateProps) + const [previousDateProps, setPreviousDateProps] = useState(dateProps) const [dates, setDates] = useState({ - ...mapDates(dateProps, previousDates, { + ...mapDates(dateProps, { dateFormat, isRange, shouldCorrectDate, @@ -65,21 +65,41 @@ export default function useDates( const hasDatePropChanges = useMemo( () => Object.keys(dateProps).some((date) => { - return dateProps[date] !== previousDates[date] + const dateProp = dateProps[date] + const previousDate = previousDateProps[date] + + const convertedDateProp = convertStringToDate(dateProp, { + dateFormat, + }) + const convertedPreviousDate = convertStringToDate(previousDate, { + dateFormat, + }) + // Make sure that same dates does not trigger a change + // i.e. 2021-01-01 and new Date('2021-01-01') + if ( + convertedDateProp instanceof Date && + convertedPreviousDate instanceof Date + ) { + return !isSameDay(convertedDateProp, convertedPreviousDate) + } + + return dateProp !== previousDate }), - [dateProps, previousDates] + [dateProps, previousDateProps, dateFormat] ) // Update dates on prop change if (hasDatePropChanges) { - setDates({ - ...mapDates({ ...dates, ...dateProps }, previousDates, { - dateFormat, - isRange, - shouldCorrectDate, - }), + const derivedDates = deriveDatesFromProps({ + dates, + dateProps, + previousDateProps, + dateFormat, + isRange, }) - setPreviousDates(dateProps) + + setDates((currentDates) => ({ ...currentDates, ...derivedDates })) + setPreviousDateProps(dateProps) } const hasHadValidDate = useRef(false) @@ -128,8 +148,8 @@ export default function useDates( // Updated input dates based on start and end dates, move to DatePickerInput // TODO: Move to DatePickerInput useEffect(() => { - const startDates = updateInputDates('start', dates) - const endDates = updateInputDates('end', dates) + const startDates = updateInputDates('start', dates.startDate) + const endDates = updateInputDates('end', dates.endDate) hasHadValidDate.current = isValid(dates.startDate) || isValid(dates.endDate) @@ -146,14 +166,13 @@ export default function useDates( dates, updateDates, hasHadValidDate: hasHadValidDate.current, - previousDates, + previousDateProps, } as const } // TODO: Move to DatePickerInput -function updateInputDates(type: 'start' | 'end', dates: DatePickerDates) { +function updateInputDates(type: 'start' | 'end', date: Date | undefined) { const updatedDates = {} - const date = dates[`${type}Date`] if (isValid(date)) { updatedDates[`__${type}Day`] = pad(format(date, 'dd'), 2) @@ -170,17 +189,13 @@ function updateInputDates(type: 'start' | 'end', dates: DatePickerDates) { function mapDates( dateProps: DatePickerDateProps, - previousDates: DatePickerDateProps, { dateFormat, isRange, shouldCorrectDate, }: Omit ) { - const date = - previousDates.date !== dateProps.date - ? dateProps.date - : previousDates.date + const date = dateProps.date const startDate = typeof dateProps?.startDate !== 'undefined' @@ -224,6 +239,7 @@ function mapDates( : {} const dates = { + date, startDate, endDate, startMonth, @@ -252,6 +268,92 @@ function mapDates( } } +function deriveDatesFromProps({ + dates, + dateProps, + previousDateProps, + dateFormat, + isRange, +}: { + dates: DatePickerDates + dateProps: DatePickerDateProps + previousDateProps: DatePickerDateProps + dateFormat: UseDatesOptions['dateFormat'] + isRange: UseDatesOptions['isRange'] +}) { + const derivedDates: DatePickerDates = {} + + const startDate = getStartDate(dateProps, previousDateProps) + + // Handle updates related to date and startDate changes when not in range mode + if (typeof startDate !== 'undefined' && startDate !== dates.startDate) { + derivedDates.startDate = + convertStringToDate(startDate, { + dateFormat, + }) || undefined + + // Set endDate and startMonth to startDate if not in range mode + if (!isRange) { + derivedDates.startMonth = + convertStringToDate(startDate, { + dateFormat, + }) || undefined + + derivedDates.endDate = derivedDates.startDate + } + } + + // update endDate based on endDate prop if in range mode + if ( + isRange && + typeof dateProps.endDate !== 'undefined' && + dateProps.endDate !== dates.endDate + ) { + derivedDates.endDate = + convertStringToDate(dateProps.endDate, { + dateFormat, + }) || undefined + } + + // Handle startMonth/endMonth + if ( + typeof dateProps.startMonth !== 'undefined' && + dateProps.startMonth !== previousDateProps.startMonth + ) { + derivedDates.startMonth = convertStringToDate(dateProps.startMonth, { + dateFormat, + }) + } + if ( + typeof dateProps.endMonth !== 'undefined' && + dateProps.endMonth !== previousDateProps.endMonth + ) { + derivedDates.endMonth = convertStringToDate(dateProps.endMonth, { + dateFormat, + }) + } + + // Handle minDate/maxDate + if ( + typeof dateProps.minDate !== 'undefined' && + dateProps.minDate !== previousDateProps.minDate + ) { + derivedDates.minDate = convertStringToDate(dateProps.minDate, { + dateFormat, + }) + } + if ( + typeof dateProps.maxDate !== 'undefined' && + dateProps.maxDate !== previousDateProps.maxDate + ) { + derivedDates.maxDate = convertStringToDate(dateProps.maxDate, { + dateFormat, + }) + } + + return derivedDates +} + function correctDates({ startDate, endDate, @@ -309,6 +411,28 @@ function getDate(date: DateType, dateFormat: string) { }) } +function getStartDate( + dateProps: DatePickerDateProps, + previousDateProps: DatePickerDateProps +) { + // priortize startDate over date if provided + if ( + typeof dateProps.startDate !== 'undefined' && + dateProps.startDate !== previousDateProps.startDate + ) { + return dateProps.startDate + } + + if ( + typeof dateProps.date !== 'undefined' && + dateProps.date !== previousDateProps.date + ) { + return dateProps.date + } + + return undefined +} + export function pad(date: string, size: number) { const dateWithPadding = '000000000' + date diff --git a/packages/dnb-eufemia/src/components/dialog/parts/DialogAction.tsx b/packages/dnb-eufemia/src/components/dialog/parts/DialogAction.tsx index 004f9573647..e8e3805c40d 100644 --- a/packages/dnb-eufemia/src/components/dialog/parts/DialogAction.tsx +++ b/packages/dnb-eufemia/src/components/dialog/parts/DialogAction.tsx @@ -1,4 +1,4 @@ -import React, { useContext } from 'react' +import React, { useCallback, useContext } from 'react' import classNames from 'classnames' import Button from '../../button/Button' import Space from '../../space/Space' @@ -71,6 +71,25 @@ const DialogAction = ({ const { close } = useContext(ModalContext) let childrenWithCloseFunc: Array + const onConfirmHandler = useCallback( + (event) => { + dispatchCustomElementEvent({ onConfirm }, 'onConfirm', { + event, + close, + }) + }, + [close, onConfirm] + ) + const onDeclineHandler = useCallback( + (event) => { + dispatchCustomElementEvent({ onDecline }, 'onDecline', { + event, + close, + }) + }, + [close, onDecline] + ) + if (children) { childrenWithCloseFunc = React.Children.map(children, (child) => { if (child.type === Button) { @@ -105,12 +124,7 @@ const DialogAction = ({ + /> ) } diff --git a/packages/dnb-eufemia/src/extensions/forms/Iterate/RemoveButton/RemoveButtonDocs.ts b/packages/dnb-eufemia/src/extensions/forms/Iterate/RemoveButton/RemoveButtonDocs.ts index 82188663e5a..dad74872682 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Iterate/RemoveButton/RemoveButtonDocs.ts +++ b/packages/dnb-eufemia/src/extensions/forms/Iterate/RemoveButton/RemoveButtonDocs.ts @@ -1,6 +1,11 @@ import { PropertiesTableProps } from '../../../../shared/types' export const RemoveButtonProperties: PropertiesTableProps = { + showConfirmDialog: { + doc: 'Use `true` to show a confirmation dialog before removing the item.', + type: 'boolean', + status: 'optional', + }, '[Button](/uilib/components/button/properties)': { doc: 'All button properties.', type: 'Various', diff --git a/packages/dnb-eufemia/src/extensions/forms/Iterate/RemoveButton/__tests__/RemoveButton.test.tsx b/packages/dnb-eufemia/src/extensions/forms/Iterate/RemoveButton/__tests__/RemoveButton.test.tsx index 09d5a950c45..2ae0d0ec52b 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Iterate/RemoveButton/__tests__/RemoveButton.test.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/Iterate/RemoveButton/__tests__/RemoveButton.test.tsx @@ -1,5 +1,6 @@ import React from 'react' import { render, fireEvent, screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' import IterateItemContext from '../../IterateItemContext' import RemoveButton from '../RemoveButton' import nbNO from '../../../constants/locales/nb-NO' @@ -16,6 +17,10 @@ describe('RemoveButton', () => { ) + afterEach(() => { + handleRemove.mockReset() + }) + it('should call handleRemove when clicked inside an Iterate element', () => { render(Remove Button, { wrapper }) @@ -120,4 +125,32 @@ describe('RemoveButton', () => { expect(screen.getByText(remove)).toBeInTheDocument() }) + + describe('showConfirmDialog', () => { + it('should show Dialog before removing', async () => { + render(, { wrapper }) + + await userEvent.click(document.querySelector('button')) + + expect(handleRemove).toHaveBeenCalledTimes(0) + + await userEvent.click( + document.querySelector( + '.dnb-dialog__inner button.dnb-button--primary' + ) + ) + + expect(handleRemove).toHaveBeenCalledTimes(1) + }) + + it('should show Dialog with translation before removing', async () => { + render(, { wrapper }) + + await userEvent.click(document.querySelector('button')) + + expect( + document.querySelector('.dnb-dialog__inner') + ).toHaveTextContent(nb.confirmRemoveText) + }) + }) }) diff --git a/packages/dnb-eufemia/src/extensions/forms/Iterate/Toolbar/Toolbar.tsx b/packages/dnb-eufemia/src/extensions/forms/Iterate/Toolbar/Toolbar.tsx index c41ab227535..8beb409f98f 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Iterate/Toolbar/Toolbar.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/Iterate/Toolbar/Toolbar.tsx @@ -6,6 +6,7 @@ import { SpaceAllProps } from '../../../../components/Space' import IterateItemContext from '../IterateItemContext' import ToolbarContext from './ToolbarContext' import FieldBoundaryContext from '../../DataContext/FieldBoundary/FieldBoundaryContext' +import ArrayItemAreaContext from '../Array/ArrayItemAreaContext' import { useTranslation } from '../../hooks' export type ToolbarParams = { @@ -27,6 +28,7 @@ export default function Toolbar({ value, arrayValue: items, } = useContext(IterateItemContext) || {} + const { toolbarVariant } = useContext(ArrayItemAreaContext) || {} const { errorInContainer } = useTranslation().IterateEditContainer const { hasError, hasVisibleError } = useContext(FieldBoundaryContext) || {} @@ -48,14 +50,17 @@ export default function Toolbar({ return ( -
    + {toolbarVariant !== 'custom' &&
    } - + {children} diff --git a/packages/dnb-eufemia/src/extensions/forms/Iterate/ViewContainer/RemoveButton.tsx b/packages/dnb-eufemia/src/extensions/forms/Iterate/ViewContainer/RemoveButton.tsx index 1f26e3467f2..6dfc5a0fad4 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Iterate/ViewContainer/RemoveButton.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/Iterate/ViewContainer/RemoveButton.tsx @@ -1,9 +1,11 @@ import React from 'react' -import RemoveButton from '../RemoveButton' +import RemoveButton, { Props as RemoveButtonProps } from '../RemoveButton' import useTranslation from '../../hooks/useTranslation' -export default function ViewContainerRemoveButton() { +export default function ViewContainerRemoveButton( + props: RemoveButtonProps +) { const { removeButton } = useTranslation().IterateViewContainer - return + return } diff --git a/packages/dnb-eufemia/src/extensions/forms/Iterate/ViewContainer/ViewContainer.tsx b/packages/dnb-eufemia/src/extensions/forms/Iterate/ViewContainer/ViewContainer.tsx index cc49e27fa4c..92fdba61dc3 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Iterate/ViewContainer/ViewContainer.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/Iterate/ViewContainer/ViewContainer.tsx @@ -16,14 +16,16 @@ export type Props = { * The title of the ViewContainer. */ title?: React.ReactNode + /** * An alternative toolbar to be shown in the ViewContainer. */ toolbar?: React.ReactNode + /** * The variant of the toolbar. */ - toolbarVariant?: 'minimumOneItem' + toolbarVariant?: ArrayItemAreaProps['toolbarVariant'] } export type AllProps = Props & FlexContainerProps & ArrayItemAreaProps @@ -62,6 +64,7 @@ function ViewContainer(props: AllProps) { mode="view" ariaLabel={convertJsxToString(itemTitle)} className={classnames('dnb-forms-section-view-block', className)} + toolbarVariant={toolbarVariant} {...restProps} > @@ -69,12 +72,13 @@ function ViewContainer(props: AllProps) { {children} {hasToolbar ? null - : toolbarElement ?? ( + : toolbarElement ?? + (toolbarVariant !== 'custom' && ( - )} + ))} ) diff --git a/packages/dnb-eufemia/src/extensions/forms/Iterate/ViewContainer/ViewContainerDocs.ts b/packages/dnb-eufemia/src/extensions/forms/Iterate/ViewContainer/ViewContainerDocs.ts index dc92473f35b..b776d570a0d 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Iterate/ViewContainer/ViewContainerDocs.ts +++ b/packages/dnb-eufemia/src/extensions/forms/Iterate/ViewContainer/ViewContainerDocs.ts @@ -7,7 +7,7 @@ export const ViewContainerProperties: PropertiesTableProps = { status: 'optional', }, variant: { - doc: 'Defines the variant of the container. Can be `outline` or `basic`. Defaults to `outline`.', + doc: 'Defines the variant of the container. Can be `outline`, `filled` or `basic`. Defaults to `outline`.', type: 'string', status: 'optional', }, @@ -17,7 +17,7 @@ export const ViewContainerProperties: PropertiesTableProps = { status: 'optional', }, toolbarVariant: { - doc: 'Use variants to render the toolbar differently. Currently there is only the `minimumOneItem` variant. See the info section for more info.', + doc: 'Use variants to render the toolbar differently. Currently there are the `minimumOneItem` and `custom` variants. See the info section for more info.', type: 'string', status: 'optional', }, diff --git a/packages/dnb-eufemia/src/extensions/forms/Iterate/hooks/useItem.ts b/packages/dnb-eufemia/src/extensions/forms/Iterate/hooks/useItem.ts index a202eac933d..befdceea108 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Iterate/hooks/useItem.ts +++ b/packages/dnb-eufemia/src/extensions/forms/Iterate/hooks/useItem.ts @@ -1,7 +1,16 @@ import { useContext } from 'react' -import IterateItemContext from '../IterateItemContext' +import IterateItemContext, { + IterateItemContextState, +} from '../IterateItemContext' -export default function useItem() { +export type UseItemReturn = Omit< + IterateItemContextState, + 'value' +> & { + value: Value +} + +export default function useItem() { const item = useContext(IterateItemContext) - return item + return item as UseItemReturn } diff --git a/packages/dnb-eufemia/src/extensions/forms/Iterate/stories/PushContainer.stories.tsx b/packages/dnb-eufemia/src/extensions/forms/Iterate/stories/PushContainer.stories.tsx index 8873adc93da..14b0c715b41 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Iterate/stories/PushContainer.stories.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/Iterate/stories/PushContainer.stories.tsx @@ -60,7 +60,7 @@ function RepresentativesEdit() { } function ExistingPersonDetails() { - const { data, getValue } = Form.useData() + const { data, getValue } = Form.useData<{ selectedPerson: string }>() const person = getValue(data.selectedPerson)?.data || {} return ( @@ -89,7 +89,7 @@ function NewPersonDetails() { } function PushContainerContent() { - const { data, update } = Form.useData() + const { data, update } = Form.useData<{ selectedPerson: string }>() // Clear the PushContainer data when the selected person is "other", // so the fields do not inherit existing data. diff --git a/packages/dnb-eufemia/src/extensions/forms/Tools/ListAllProps.tsx b/packages/dnb-eufemia/src/extensions/forms/Tools/ListAllProps.tsx index 301a0c47fa6..0fda1113732 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Tools/ListAllProps.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/Tools/ListAllProps.tsx @@ -2,19 +2,22 @@ import { isValidElement, useCallback, useContext, useRef } from 'react' import pointer, { JsonObject } from '../utils/json-pointer' import DataContext, { FilterData } from '../DataContext/Context' -export type ListAllPropsReturn = { - propsOfFields: JsonObject - propsOfValues: JsonObject +export type ListAllPropsReturn = { + propsOfFields: Data + propsOfValues: Data } -export type ListAllPropsProps = { +export type ListAllPropsProps = { log?: boolean - generateRef?: React.MutableRefObject<() => ListAllPropsReturn> + generateRef?: React.MutableRefObject<() => ListAllPropsReturn> filterData?: FilterData children: React.ReactNode } -export type GenerateRef = ListAllPropsProps['generateRef']['current'] +export type GenerateRef = + ListAllPropsProps['generateRef']['current'] -export default function ListAllProps(props: ListAllPropsProps) { +export default function ListAllProps( + props: ListAllPropsProps +) { const { log, generateRef, filterData, children } = props || {} const { fieldPropsRef, valuePropsRef, data, hasContext } = useContext(DataContext) @@ -71,7 +74,7 @@ export default function ListAllProps(props: ListAllPropsProps) { return acc }, {}) - return { propsOfFields, propsOfValues } as ListAllPropsReturn + return { propsOfFields, propsOfValues } as ListAllPropsReturn }, [fieldPropsRef, filterData, valuePropsRef]) if (hasContext) { diff --git a/packages/dnb-eufemia/src/extensions/forms/Tools/__tests__/ListAllProps.test.tsx b/packages/dnb-eufemia/src/extensions/forms/Tools/__tests__/ListAllProps.test.tsx index 07e7d3e622e..5326d707cda 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Tools/__tests__/ListAllProps.test.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/Tools/__tests__/ListAllProps.test.tsx @@ -635,7 +635,17 @@ describe('Tools.ListAllProps', () => { }) it('should filter out React elements', () => { - const generateRef = React.createRef() + const generateRef = React.createRef< + GenerateRef<{ + items: { + children: { + type: { + name: string + } + } + } + }> + >() render( diff --git a/packages/dnb-eufemia/src/extensions/forms/Value/PhoneNumber/PhoneNumber.tsx b/packages/dnb-eufemia/src/extensions/forms/Value/PhoneNumber/PhoneNumber.tsx index 27bda94a6dd..28f2e42fa08 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Value/PhoneNumber/PhoneNumber.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/Value/PhoneNumber/PhoneNumber.tsx @@ -1,9 +1,6 @@ import React, { useCallback } from 'react' import StringValue, { Props as StringValueProps } from '../String' -import { - format, - cleanNumber, -} from '../../../../components/number-format/NumberUtils' +import { format } from '../../../../components/number-format/NumberUtils' import useTranslation from '../../hooks/useTranslation' export type Props = StringValueProps @@ -15,7 +12,8 @@ function PhoneNumber(props: Props) { props.label ?? (props.inline ? undefined : translations.label) const toInput = useCallback((value) => { - return format(cleanNumber(value), { + // We can't use the "cleanNumber" function here, because we need to keep the country code separate from the number + return format(value, { phone: true, }).toString() }, []) diff --git a/packages/dnb-eufemia/src/extensions/forms/Wizard/Container/WizardContainer.tsx b/packages/dnb-eufemia/src/extensions/forms/Wizard/Container/WizardContainer.tsx index 3f3fc8d4571..4b13a5a2f32 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Wizard/Container/WizardContainer.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/Wizard/Container/WizardContainer.tsx @@ -30,6 +30,7 @@ import DataContext, { import Handler from '../../Form/Handler/Handler' import { SharedStateReturn, + createReferenceKey, useSharedState, } from '../../../../shared/helpers/useSharedState' import useHandleLayoutEffect from './useHandleLayoutEffect' @@ -146,7 +147,7 @@ function WizardContainer(props: Props) { > >() sharedStateRef.current = useSharedState( - hasContext && id ? id + '-wizard' : undefined + hasContext && id ? createReferenceKey(id, 'wizard') : undefined ) // Store the current state of showAllErrors diff --git a/packages/dnb-eufemia/src/extensions/forms/Wizard/hooks/useStep.tsx b/packages/dnb-eufemia/src/extensions/forms/Wizard/hooks/useStep.tsx index e0e94811670..b3753b75a05 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Wizard/hooks/useStep.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/Wizard/hooks/useStep.tsx @@ -3,15 +3,18 @@ import WizardContext, { OnStepChange, WizardContextState, } from '../Context/WizardContext' -import { Identifier } from '../../types' -import { useSharedState } from '../../../../shared/helpers/useSharedState' +import { + SharedStateId, + createReferenceKey, + useSharedState, +} from '../../../../shared/helpers/useSharedState' // SSR warning fix: https://gist.github.com/gaearon/e7d97cdf38a2907924ea12e4ebdf3c85 const useLayoutEffect = typeof window === 'undefined' ? React.useEffect : React.useLayoutEffect export default function useStep( - id: Identifier = null, + id: SharedStateId = null, { onStepChange }: { onStepChange?: OnStepChange } = {} ) { const setFormError = useCallback(() => null, []) @@ -26,7 +29,7 @@ export default function useStep( const sharedDataRef = useRef>>(null) sharedDataRef.current = useSharedState( - id ? id + '-wizard' : undefined + id ? createReferenceKey(id, 'wizard') : undefined ) useLayoutEffect(() => { diff --git a/packages/dnb-eufemia/src/extensions/forms/Wizard/style/themes/dnb-wizard-layout-theme-ui.scss b/packages/dnb-eufemia/src/extensions/forms/Wizard/style/themes/dnb-wizard-layout-theme-ui.scss index abdabed701d..d676d7d91d2 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Wizard/style/themes/dnb-wizard-layout-theme-ui.scss +++ b/packages/dnb-eufemia/src/extensions/forms/Wizard/style/themes/dnb-wizard-layout-theme-ui.scss @@ -1,6 +1,8 @@ .dnb-forms-wizard-layout { &__contents { .dnb-card { + // Change the default outline color to match the StepIndicator v2. + // This is deprecated and can be removed when the StepIndicator v3 (without a sidebar) is released. --card-outline-color: var(--color-pistachio); } } diff --git a/packages/dnb-eufemia/src/extensions/forms/constants/countries.ts b/packages/dnb-eufemia/src/extensions/forms/constants/countries.ts index f4acbd8d33a..c0e0e90fe74 100644 --- a/packages/dnb-eufemia/src/extensions/forms/constants/countries.ts +++ b/packages/dnb-eufemia/src/extensions/forms/constants/countries.ts @@ -1650,7 +1650,7 @@ const countries: Array = [ en: 'Puerto Rico', nb: 'Puerto Rico', }, - cdc: '1-787, 1-939', + cdc: '1-787', iso: 'PR', continent: 'North America', }, 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 a44eb1df661..d2df5c1b137 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 @@ -33,6 +33,7 @@ export default { }, RemoveButton: { text: 'Remove', + confirmRemoveText: 'Are you sure you want to delete this?', }, 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 b976462b78b..1110f5caee2 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 @@ -32,6 +32,7 @@ export default { }, RemoveButton: { text: 'Fjern', + confirmRemoveText: 'Er du sikker på at du vil slette dette?', }, SectionViewContainer: { editButton: 'Endre', diff --git a/packages/dnb-eufemia/src/extensions/forms/hooks/useDataContext.tsx b/packages/dnb-eufemia/src/extensions/forms/hooks/useDataContext.tsx index a8116150377..9c2f020983a 100644 --- a/packages/dnb-eufemia/src/extensions/forms/hooks/useDataContext.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/hooks/useDataContext.tsx @@ -1,6 +1,7 @@ import { useCallback, useContext } from 'react' import { SharedStateId, + createReferenceKey, useSharedState, } from '../../../shared/helpers/useSharedState' import DataContext, { ContextState } from '../DataContext/Context' @@ -10,7 +11,7 @@ export default function useDataContext(id: SharedStateId = undefined): { getContext: () => ContextState } { const sharedDataContext = useSharedState( - id + '-data-context' + createReferenceKey(id, 'data-context') ) const dataContext = useContext(DataContext) diff --git a/packages/dnb-eufemia/src/extensions/forms/hooks/useSnapshot.tsx b/packages/dnb-eufemia/src/extensions/forms/hooks/useSnapshot.tsx index 62da13025d8..b56f06bc2f5 100644 --- a/packages/dnb-eufemia/src/extensions/forms/hooks/useSnapshot.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/hooks/useSnapshot.tsx @@ -1,13 +1,13 @@ import { useCallback, useRef } from 'react' import { makeUniqueId } from '../../../shared/component-helper' -import pointer from '../utils/json-pointer' +import pointer, { JsonObject } from '../utils/json-pointer' import { SharedStateId } from '../../../shared/helpers/useSharedState' import useDataContext from './useDataContext' import { SnapshotId, SnapshotName } from '../Form/Snapshot' import useData from '../Form/data-context/useData' export default function useSnapshot(id?: SharedStateId) { - const internalSnapshotsRef = useRef>() + const internalSnapshotsRef = useRef>() if (!internalSnapshotsRef.current) { internalSnapshotsRef.current = new Map() } @@ -19,14 +19,14 @@ export default function useSnapshot(id?: SharedStateId) { ( id: SnapshotId = makeUniqueId(), name: SnapshotName = null, - content: unknown = null + content: JsonObject = null ): SnapshotId => { const { internalDataRef, snapshotsRef } = getContext() if (!content) { const snapshotWithPaths = snapshotsRef?.current?.get?.(name) if (snapshotWithPaths) { - const collectedData = new Map() + const collectedData: Map = new Map() snapshotWithPaths.forEach((isMounted, path) => { if (isMounted && pointer.has(internalDataRef.current, path)) { collectedData.set( @@ -35,7 +35,7 @@ export default function useSnapshot(id?: SharedStateId) { ) } }) - content = collectedData + content = collectedData as unknown as JsonObject } else { content = internalDataRef.current } @@ -52,7 +52,7 @@ export default function useSnapshot(id?: SharedStateId) { ) const getSnapshot = useCallback( - (id: SnapshotId, name: SnapshotName = null): unknown => { + (id: SnapshotId, name: SnapshotName = null): JsonObject => { return internalSnapshotsRef.current.get(combineIdWithName(id, name)) }, [] diff --git a/packages/dnb-eufemia/src/extensions/forms/types.ts b/packages/dnb-eufemia/src/extensions/forms/types.ts index 3228bb4162a..89bbcf96a57 100644 --- a/packages/dnb-eufemia/src/extensions/forms/types.ts +++ b/packages/dnb-eufemia/src/extensions/forms/types.ts @@ -562,21 +562,18 @@ export type EventReturnWithStateObjectAndSuccess = | Error | EventStateObjectWithSuccess -export type OnSubmitParams = { +export type OnSubmitParams = { /** Will remove data entries of fields that are not visible */ reduceToVisibleFields: ( - data: JsonObject, + data: Data, options?: VisibleDataOptions - ) => Partial + ) => Partial /** Will call the given function for each data path. The returned `value` will replace each data entry. It's up to you to define the shape of the value. */ - transformData: ( - data: JsonObject, - handler: TransformData - ) => TransformData + transformData: (data: Data, handler: TransformData) => Partial /** Will filter data based on the given "filterDataHandler" method */ - filterData: (filterDataHandler: FilterData) => Partial + filterData: (filterDataHandler: FilterData) => Partial /** Will remove browser-side stored autocomplete data */ resetForm: () => void diff --git a/packages/dnb-eufemia/src/extensions/forms/utils/json-pointer/json-pointer.ts b/packages/dnb-eufemia/src/extensions/forms/utils/json-pointer/json-pointer.ts index 0e921caf43f..5ca5485846f 100644 --- a/packages/dnb-eufemia/src/extensions/forms/utils/json-pointer/json-pointer.ts +++ b/packages/dnb-eufemia/src/extensions/forms/utils/json-pointer/json-pointer.ts @@ -1,8 +1,8 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ export type PointerPath = string | Array -export type JsonValue = any -export type JsonObject = any +export type JsonValue = unknown +export type JsonObject = Record | Array /** * Lookup a json pointer in an object diff --git a/packages/dnb-eufemia/src/shared/helpers/__tests__/useSharedState.test.ts b/packages/dnb-eufemia/src/shared/helpers/__tests__/useSharedState.test.ts index 104ee5ffc67..5fe8b8e566a 100644 --- a/packages/dnb-eufemia/src/shared/helpers/__tests__/useSharedState.test.ts +++ b/packages/dnb-eufemia/src/shared/helpers/__tests__/useSharedState.test.ts @@ -1,9 +1,15 @@ import { renderHook, act } from '@testing-library/react' import { makeUniqueId } from '../../component-helper' -import { useSharedState, createSharedState } from '../useSharedState' +import { + useSharedState, + createSharedState, + SharedStateId, + createReferenceKey, +} from '../useSharedState' +import { createContext } from 'react' describe('useSharedState', () => { - let identifier: string + let identifier: SharedStateId beforeEach(() => { identifier = makeUniqueId() @@ -47,6 +53,50 @@ describe('useSharedState', () => { expect(result.current.data).toEqual({ test: 'changed' }) }) + it('should update the shared state with a function reference as id', () => { + const identifier = () => null + const { result } = renderHook(() => + useSharedState(identifier, { test: 'initial' }) + ) + act(() => { + result.current.update({ test: 'updated' }) + }) + expect(result.current.data).toEqual({ test: 'updated' }) + }) + + it('should update the shared state with an async function reference as id', () => { + const identifier = async () => null + const { result } = renderHook(() => + useSharedState(identifier, { test: 'initial' }) + ) + act(() => { + result.current.update({ test: 'updated' }) + }) + expect(result.current.data).toEqual({ test: 'updated' }) + }) + + it('should update the shared state with an object reference as id', () => { + const identifier = {} + const { result } = renderHook(() => + useSharedState(identifier, { test: 'initial' }) + ) + act(() => { + result.current.update({ test: 'updated' }) + }) + expect(result.current.data).toEqual({ test: 'updated' }) + }) + + it('should update the shared state with a React context reference as id', () => { + const identifier = createContext(null) + const { result } = renderHook(() => + useSharedState(identifier, { test: 'initial' }) + ) + act(() => { + result.current.update({ test: 'updated' }) + }) + expect(result.current.data).toEqual({ test: 'updated' }) + }) + it('should unsubscribe from the shared state when the component unmounts', () => { const { result, unmount } = renderHook(() => useSharedState(identifier, { test: 'initial' }) @@ -171,3 +221,66 @@ describe('useSharedState', () => { expect(resultB.current.data).toEqual({ foo: 'baz' }) }) }) + +describe('createReferenceKey', () => { + it('should return the same object for the same references', () => { + const ref1 = {} + const ref2 = () => null + + const key1 = createReferenceKey(ref1, ref2) + const key2 = createReferenceKey(ref1, ref2) + + expect(key1).toBe(key2) + }) + + it('should return the same object for the same string references', () => { + const ref1 = {} + const ref2 = 'unique' + + const key1 = createReferenceKey(ref1, ref2) + const key2 = createReferenceKey(ref1, ref2) + + expect(key1).toBe(key2) + }) + + it('should return different objects for different references', () => { + const ref1 = {} + const ref2 = {} + const ref3 = {} + + const key1 = createReferenceKey(ref1, ref2) + const key2 = createReferenceKey(ref1, ref3) + + expect(key1).not.toBe(key2) + }) + + it('should return different objects for different first references', () => { + const ref1 = {} + const ref2 = {} + const ref3 = {} + + const key1 = createReferenceKey(ref1, ref2) + const key2 = createReferenceKey(ref3, ref2) + + expect(key1).not.toBe(key2) + }) + + it('should cache the combined reference', () => { + const ref1 = {} + const ref2 = () => null + + const key1 = createReferenceKey(ref1, ref2) + const key2 = createReferenceKey(ref1, ref2) + + expect(key1).toBe(key2) + }) + + it('should create a new reference if it does not exist', () => { + const ref1 = {} + const ref2 = {} + + const key1 = createReferenceKey(ref1, ref2) + + expect(key1).toBeDefined() + }) +}) diff --git a/packages/dnb-eufemia/src/shared/helpers/useSharedState.tsx b/packages/dnb-eufemia/src/shared/helpers/useSharedState.tsx index 4fb9fe76bdf..6d837bceb3f 100644 --- a/packages/dnb-eufemia/src/shared/helpers/useSharedState.tsx +++ b/packages/dnb-eufemia/src/shared/helpers/useSharedState.tsx @@ -12,20 +12,22 @@ import useMountEffect from './useMountEffect' const useLayoutEffect = typeof window === 'undefined' ? React.useEffect : React.useLayoutEffect -export type SharedStateId = string +export type SharedStateId = + | string + | (() => void) + | Promise<() => void> + | React.Context + | Record /** * Custom hook that provides shared state functionality. - * - * @template Data - The type of data stored in the shared state. - * @param {SharedStateId} id - The identifier for the shared state. - * @param {Data} initialData - The initial data for the shared state. - * @param {Function} onChange - Optional callback function to be called when the shared state is set from another instance/component. - * @returns {Object} - An object containing the shared state data, update function, extend function, and set function. */ export function useSharedState( - id: SharedStateId, + /** The identifier for the shared state. */ + id: SharedStateId | undefined, + /** The initial data for the shared state. */ initialData: Data = undefined, + /** Optional callback function to be called when the shared state is set from another instance/component. */ onChange = null ) { const [, forceUpdate] = useReducer(() => ({}), {}) @@ -53,7 +55,7 @@ export function useSharedState( }, [id, initialData]) const sharedAttachment = useMemo(() => { if (id) { - return createSharedState(id + '-oc', { onChange }) + return createSharedState(createReferenceKey(id, 'oc'), { onChange }) } }, [id, onChange]) @@ -141,26 +143,27 @@ interface SharedStateInstance extends SharedStateReturn { hadInitialData: boolean } -const sharedStates: Record> = {} +const sharedStates: Map< + SharedStateId, + SharedStateInstance +> = new Map() /** * Creates a shared state instance with the specified ID and initial data. - * @template Data The type of data stored in the shared state. - * @param id The ID of the shared state. - * @param initialData The initial data for the shared state. - * @returns The created shared state instance. */ export function createSharedState( + /** The identifier for the shared state. */ id: SharedStateId, + /** The initial data for the shared state. */ initialData?: Data ): SharedStateInstance { - if (!sharedStates[id]) { + if (!sharedStates.get(id)) { let subscribers: Subscriber[] = [] - const get = () => sharedStates[id].data + const get = () => sharedStates.get(id).data const set = (newData: Partial) => { - sharedStates[id].data = { ...newData } + sharedStates.get(id).data = { ...newData } } const update = (newData: Partial) => { @@ -169,7 +172,10 @@ export function createSharedState( } const extend = (newData: Data) => { - sharedStates[id].data = { ...sharedStates[id].data, ...newData } + sharedStates.get(id).data = { + ...sharedStates.get(id).data, + ...newData, + } sync() } @@ -187,7 +193,7 @@ export function createSharedState( subscribers.forEach((subscriber) => subscriber()) } - sharedStates[id] = { + sharedStates.set(id, { data: undefined, get, set, @@ -196,17 +202,36 @@ export function createSharedState( subscribe, unsubscribe, hadInitialData: Boolean(initialData), - } as SharedStateInstance + } as SharedStateInstance) if (initialData) { extend(initialData) } } else if ( - sharedStates[id].data === undefined && + sharedStates.get(id).data === undefined && initialData !== undefined ) { - sharedStates[id].data = { ...initialData } + sharedStates.get(id).data = { ...initialData } + } + + return sharedStates.get(id) +} + +/** + * Creates a reference key for the shared state. + * You can pass any JavaScript instance as the reference. + */ +export function createReferenceKey(ref1, ref2) { + if (!cache.has(ref1)) { + cache.set(ref1, new Map()) + } + + const innerMap = cache.get(ref1) + + if (!innerMap.has(ref2)) { + innerMap.set(ref2, {}) } - return sharedStates[id] + return innerMap.get(ref2) } +const cache = new Map() diff --git a/packages/dnb-eufemia/src/style/themes/theme-sbanken/sbanken-theme-forms.scss b/packages/dnb-eufemia/src/style/themes/theme-sbanken/sbanken-theme-forms.scss index e41a420d08e..326e15d8a38 100644 --- a/packages/dnb-eufemia/src/style/themes/theme-sbanken/sbanken-theme-forms.scss +++ b/packages/dnb-eufemia/src/style/themes/theme-sbanken/sbanken-theme-forms.scss @@ -17,3 +17,4 @@ $THEME_FALLBACK: 'ui'; @import '../../../extensions/forms/Field/Number/style/themes/dnb-number-theme-sbanken.scss'; @import '../../../extensions/forms/FieldBlock/style/themes/dnb-field-block-theme-sbanken.scss'; @import '../../../extensions/forms/Wizard/style/themes/dnb-wizard-layout-theme-sbanken.scss'; +@import '../../../extensions/forms/Form/Section/style/themes/dnb-section-theme-sbanken.scss'; diff --git a/packages/dnb-eufemia/src/style/themes/theme-ui/ui-theme-forms.scss b/packages/dnb-eufemia/src/style/themes/theme-ui/ui-theme-forms.scss index f88b254164d..7215604584f 100644 --- a/packages/dnb-eufemia/src/style/themes/theme-ui/ui-theme-forms.scss +++ b/packages/dnb-eufemia/src/style/themes/theme-ui/ui-theme-forms.scss @@ -17,3 +17,4 @@ $THEME_FALLBACK: 'ui'; @import '../../../extensions/forms/Field/Number/style/themes/dnb-number-theme-ui.scss'; @import '../../../extensions/forms/FieldBlock/style/themes/dnb-field-block-theme-ui.scss'; @import '../../../extensions/forms/Wizard/style/themes/dnb-wizard-layout-theme-ui.scss'; +@import '../../../extensions/forms/Form/Section/style/themes/dnb-section-theme-ui.scss';