diff --git a/packages/dnb-design-system-portal/src/docs/uilib/components/upload/demos.mdx b/packages/dnb-design-system-portal/src/docs/uilib/components/upload/demos.mdx
index bd88c286958..a0d1fd25799 100644
--- a/packages/dnb-design-system-portal/src/docs/uilib/components/upload/demos.mdx
+++ b/packages/dnb-design-system-portal/src/docs/uilib/components/upload/demos.mdx
@@ -32,13 +32,13 @@ You can also use the file blob in combination with the [FileReader](https://deve
### Upload loading state
-When uploading the file you can set the loading state of the request using the `Upload.useUpload` hook and passing isLoading to the file that is being uploaded.
+When uploading the file you can set the loading state of the request using the `Upload.useUpload` hook and passing `isLoading` to the file that is being uploaded.
### Upload error message
-The only checks we do currently is for the file size and the file type. These errors are handled by the HTML element ´input´ so they aren't selectable. If you want any other error messages you can use the `Upload.useUpload` the same way as with the loading state.
+The only file verification the Upload component does is for the file size and the file type. These errors are handled by the HTML element `input` so they aren't selectable. If you want any other error messages you can use the `Upload.useUpload` hook the same way as with the loading state.
diff --git a/packages/dnb-design-system-portal/src/docs/uilib/components/upload/events.mdx b/packages/dnb-design-system-portal/src/docs/uilib/components/upload/events.mdx
index 434952b0c81..c03e28485cd 100644
--- a/packages/dnb-design-system-portal/src/docs/uilib/components/upload/events.mdx
+++ b/packages/dnb-design-system-portal/src/docs/uilib/components/upload/events.mdx
@@ -2,11 +2,11 @@
showTabs: true
---
+import PropertiesTable from 'dnb-design-system-portal/src/shared/parts/PropertiesTable'
+import { UploadEvents } from '@dnb/eufemia/src/components/upload/UploadDocs'
+
## Events
-| Events | Description |
-| -------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
-| `onChange` | _(optional)_ will be called on `files` changes made by the user. Access the files with `{ files }` (containing each a `fileItem`). |
-| `onFileDelete` | _(optional)_ will be called once a file gets deleted by the user. Access the deleted file with `{ fileItem }`. |
+
Each `fileItem` will contain a `{ file, id }` (File Object and an unique ID) along with other information.
diff --git a/packages/dnb-design-system-portal/src/docs/uilib/components/upload/info.mdx b/packages/dnb-design-system-portal/src/docs/uilib/components/upload/info.mdx
index 506565f0c36..de771b1e274 100644
--- a/packages/dnb-design-system-portal/src/docs/uilib/components/upload/info.mdx
+++ b/packages/dnb-design-system-portal/src/docs/uilib/components/upload/info.mdx
@@ -10,7 +10,7 @@ The Upload component should be used in scenarios where the user has to upload an
- Files selected by the user should be uploaded immediately (temporary location).
- The user should be able to remove them (files) during the session.
-- If the Upload component is shown in a submit form, then a [GlobalStatus](/uilib/components/global-status) should be a part of the form.
+- The Upload component connects to the [GlobalStatus](/uilib/components/global-status) and displays file error messages there as well.
- Validation messages coming from the "backend" should be displayed for each file via the `useUpload` hook. See [example](/uilib/components/upload/#upload-error-message) below.
- The `useUpload` hook can be placed on any location in your application, and does not need to be where the `Upload` component is used.
@@ -33,6 +33,10 @@ function YourComponent() {
}
```
+## JPG vs JPEG
+
+When `jpg` is defined (most commonly used), then the component will also accept `jpeg` files.
+
## Backend integration
The "backend" receiving the files is decoupled and can be any existing or new system.
diff --git a/packages/dnb-design-system-portal/src/docs/uilib/components/upload/properties.mdx b/packages/dnb-design-system-portal/src/docs/uilib/components/upload/properties.mdx
index 99c1302d50f..c506d6001ca 100644
--- a/packages/dnb-design-system-portal/src/docs/uilib/components/upload/properties.mdx
+++ b/packages/dnb-design-system-portal/src/docs/uilib/components/upload/properties.mdx
@@ -2,28 +2,16 @@
showTabs: true
---
+import TranslationsTable from 'dnb-design-system-portal/src/shared/parts/TranslationsTable'
+import PropertiesTable from 'dnb-design-system-portal/src/shared/parts/PropertiesTable'
+import { UploadProperties } from '@dnb/eufemia/src/components/upload/UploadDocs'
+
## Properties
-| Properties | Description |
-| --------------------------------------- | ----------------------------------------------------------------------------------------- |
-| `acceptedFileTypes` | _(required)_ List of accepted file types. More details above. |
-| `filesAmountLimit` | _(optional)_ Defines the amount of files the user can select and upload. Defaults to 100. |
-| `fileMaxSize` | _(optional)_ `fileMaxSize` is max size of each file in MB. Defaults to 5 MB. |
-| `title` | _(optional)_ Custom text property. Replaces the default title. |
-| `text` | _(optional)_ Custom text property. Replaces the default text. |
-| `fileTypeDescription` | _(optional)_ Custom text property. Replaces the default accepted format description. |
-| `fileSizeDescription` | _(optional)_ Custom text property. Replaces the default max file size description. |
-| `fileSizeContent` | _(optional)_ Custom text property. Replaces the default file size content. |
-| `buttonText` | _(optional)_ Custom text property. Replaces the default upload button text. |
-| `loadingText` | _(optional)_ Custom text property. Replaces the default loading text. |
-| `errorLargeFile` | _(optional)_ Custom text property. Replaces the default file size error message. |
-| `errorUnsupportedFile` | _(optional)_ Custom text property. Replaces the default file type error message. |
-| `errorAmountLimit` | _(optional)_ Custom text property. Replaces the default amount limit error message. |
-| `deleteButton` | _(optional)_ Custom text property. Replaces the default delete button text. |
-| `fileListAriaLabel` | _(optional)_ Custom text property. Replaces the default list aria label. |
-| `skeleton` | _(optional)_ Skeleton should be applied when loading content Default: `null`. |
-| [Space](/uilib/layout/space/properties) | _(optional)_ Spacing properties like `top` or `bottom` are supported. |
+
+
+## Translations
-## JPG vs JPEG
+All translation keys listed in the translations table below, can be used as a component property (like `title` or `text`).
-When `jpg` is defined (most commonly used), then the component will also accept `jpeg` files.
+
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 382137a08f5..1aea4113039 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,10 @@ breadcrumb:
Change log for the Eufemia Forms extension.
+## v10.41
+
+- Added [Field.Upload](/uilib/extensions/forms/Field/Upload/) component.
+
## v10.38
- Added support for nesting fields inside of [Form.Section](/uilib/extensions/forms/Form/Section/) and [Form.ArraySelection](/uilib/extensions/forms/Form/ArraySelection/).
diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/feature-fields/Slider/info.mdx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/feature-fields/Slider/info.mdx
index f932740efeb..fea631b429a 100644
--- a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/feature-fields/Slider/info.mdx
+++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/feature-fields/Slider/info.mdx
@@ -11,7 +11,7 @@ import { Field } from '@dnb/eufemia/extensions/forms'
render()
```
-## Multithub support
+## Multithumb support
You can use the `paths` property to define an array with JSON Pointers for multiple thumb buttons.
diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/feature-fields/Upload.mdx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/feature-fields/Upload.mdx
new file mode 100644
index 00000000000..df1e3685efc
--- /dev/null
+++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/feature-fields/Upload.mdx
@@ -0,0 +1,29 @@
+---
+title: 'Upload'
+description: '`Field.Upload` is a wrapper for the Upload component to make it easier to use inside a form.'
+showTabs: true
+theme: 'sbanken'
+hideInMenu: true
+tabs:
+ - title: Info
+ key: '/info'
+ - title: Demos
+ key: '/demos'
+ - title: Properties
+ key: '/properties'
+ - title: Events
+ key: '/events'
+breadcrumb:
+ - text: Forms
+ href: /uilib/extensions/forms/
+ - text: Feature fields
+ href: /uilib/extensions/forms/feature-fields/
+ - text: Upload
+ href: /uilib/extensions/forms/feature-fields/Upload/
+---
+
+import Info from 'Docs/uilib/extensions/forms/feature-fields/Upload/info'
+import Demos from 'Docs/uilib/extensions/forms/feature-fields/Upload/demos'
+
+
+
diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/feature-fields/Upload/Examples.tsx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/feature-fields/Upload/Examples.tsx
new file mode 100644
index 00000000000..ff1e5e18130
--- /dev/null
+++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/feature-fields/Upload/Examples.tsx
@@ -0,0 +1,89 @@
+import { Flex } from '@dnb/eufemia/src'
+import ComponentBox from '../../../../../../shared/tags/ComponentBox'
+import { Field, Form } from '@dnb/eufemia/src/extensions/forms'
+
+export const BasicUsage = () => {
+ return (
+
+
+
+
+
+ )
+}
+
+export const Required = () => {
+ return (
+
+ console.log('onSubmit', data)}>
+
+
+
+
+
+
+ )
+}
+
+export const WithHelp = () => {
+ return (
+
+
+
+ )
+}
+
+export const Customized = () => {
+ return (
+
+
+
+ )
+}
+
+export const WithPath = () => {
+ const createMockFile = (name: string, size: number, type: string) => {
+ const file = new File([], name, { type })
+ Object.defineProperty(file, 'size', {
+ get() {
+ return size
+ },
+ })
+ return file
+ }
+
+ return (
+
+ console.log('onChange', data)}
+ data={{
+ myFiles: [
+ { file: createMockFile('fileName-1.png', 100, 'image/png') },
+ ],
+ }}
+ >
+
+
+
+ )
+}
diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/feature-fields/Upload/demos.mdx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/feature-fields/Upload/demos.mdx
new file mode 100644
index 00000000000..58b2f44838d
--- /dev/null
+++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/feature-fields/Upload/demos.mdx
@@ -0,0 +1,27 @@
+---
+showTabs: true
+---
+
+import * as Examples from './Examples'
+
+## Demos
+
+### Basic usage
+
+
+
+### Required
+
+
+
+### Path usage
+
+
+
+### With help
+
+
+
+### Customized
+
+
diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/feature-fields/Upload/events.mdx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/feature-fields/Upload/events.mdx
new file mode 100644
index 00000000000..ff13268a03a
--- /dev/null
+++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/feature-fields/Upload/events.mdx
@@ -0,0 +1,10 @@
+---
+showTabs: true
+---
+
+import PropertiesTable from 'dnb-design-system-portal/src/shared/parts/PropertiesTable'
+import { UploadFieldEvents } from '@dnb/eufemia/src/extensions/forms/Field/Upload/UploadDocs'
+
+## Events
+
+
diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/feature-fields/Upload/info.mdx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/feature-fields/Upload/info.mdx
new file mode 100644
index 00000000000..16f40b7d6a0
--- /dev/null
+++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/feature-fields/Upload/info.mdx
@@ -0,0 +1,56 @@
+---
+showTabs: true
+---
+
+## Description
+
+`Field.Upload` is a wrapper for the [Upload](/uilib/components/upload/) component to make it easier to use inside a form.
+
+```jsx
+import { Field } from '@dnb/eufemia/extensions/forms'
+render()
+```
+
+## The data and file format
+
+The returned data is an array of objects containing a file object and a unique ID. The file object contains the file itself and some additional properties like an unique ID.
+
+```tsx
+{
+ id: '1234',
+ file: {
+ name: 'file1.jpg',
+ size: 1234,
+ type: 'image/jpeg',
+ },
+ errorMessage: 'error message ...',
+}
+```
+
+This data format will be returned by the `onChange` and the `onSubmit` event handlers.
+
+## Validation
+
+The `required` prop will validate if there are valid files present. If there are files with an error, the validation will fail.
+
+If there are invalid files, the `onSubmit` event will not be called and a validation error will be shown.
+
+The `onChange` event handler will return an array with objects containing the file object and some additional properties – regardless of the validity of the file.
+
+## About the `value` and `path` prop
+
+The `path` prop represents an array with an object described above:
+
+```tsx
+render(
+
+
+ ,
+)
+```
+
+The `value` prop represents an array with an object described above:
+
+```tsx
+render()
+```
diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/feature-fields/Upload/properties.mdx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/feature-fields/Upload/properties.mdx
new file mode 100644
index 00000000000..f6069384a86
--- /dev/null
+++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/feature-fields/Upload/properties.mdx
@@ -0,0 +1,26 @@
+---
+showTabs: true
+---
+
+import TranslationsTable from 'dnb-design-system-portal/src/shared/parts/TranslationsTable'
+import PropertiesTable from 'dnb-design-system-portal/src/shared/parts/PropertiesTable'
+import { fieldProperties } from '@dnb/eufemia/src/extensions/forms/Field/FieldDocs'
+import { UploadFieldProperties } from '@dnb/eufemia/src/extensions/forms/Field/Upload/UploadDocs'
+
+## Properties
+
+### Field-specific props
+
+
+
+### General props
+
+']}
+ omit={['layout', 'onBlurValidator', 'contentWidth']}
+/>
+
+## Translations
+
+
diff --git a/packages/dnb-design-system-portal/src/shared/parts/PropertiesTable.tsx b/packages/dnb-design-system-portal/src/shared/parts/PropertiesTable.tsx
index 930615e24ec..52ea1f429b8 100644
--- a/packages/dnb-design-system-portal/src/shared/parts/PropertiesTable.tsx
+++ b/packages/dnb-design-system-portal/src/shared/parts/PropertiesTable.tsx
@@ -75,8 +75,8 @@ export default function PropertiesTable({
omit?: string[]
showDefaultValue: boolean
}) {
- const keys = Object.keys(props)
- const tableRows = Object.entries(props).map(([key, props]) => {
+ const keys = Object.keys(props || {})
+ const tableRows = Object.entries(props || {}).map(([key, props]) => {
if (!props) {
return null
}
diff --git a/packages/dnb-eufemia/src/components/upload/Upload.tsx b/packages/dnb-eufemia/src/components/upload/Upload.tsx
index 2d8b97f16d5..380838169fd 100644
--- a/packages/dnb-eufemia/src/components/upload/Upload.tsx
+++ b/packages/dnb-eufemia/src/components/upload/Upload.tsx
@@ -21,6 +21,7 @@ import type { UploadFile, UploadAllProps } from './types'
import UploadFileList from './UploadFileList'
import UploadInfo from './UploadInfo'
+export type * from './types'
export { defaultProps }
const Upload = (localProps: UploadAllProps) => {
diff --git a/packages/dnb-eufemia/src/components/upload/UploadDocs.ts b/packages/dnb-eufemia/src/components/upload/UploadDocs.ts
new file mode 100644
index 00000000000..8076990aed6
--- /dev/null
+++ b/packages/dnb-eufemia/src/components/upload/UploadDocs.ts
@@ -0,0 +1,52 @@
+import { PropertiesTableProps } from '../../shared/types'
+
+export const UploadProperties: PropertiesTableProps = {
+ acceptedFileTypes: {
+ doc: 'List of accepted file types.',
+ type: 'Array',
+ status: 'required',
+ },
+ filesAmountLimit: {
+ doc: 'Defines the amount of files the user can select and upload. Defaults to 100.',
+ type: 'number',
+ status: 'optional',
+ },
+ fileMaxSize: {
+ doc: '`fileMaxSize` is max size of each file in MB. Defaults to 5 MB.',
+ type: 'number',
+ status: 'optional',
+ },
+ title: {
+ doc: 'Custom text property. Replaces the default title.',
+ type: 'string',
+ status: 'optional',
+ },
+ text: {
+ doc: 'Custom text property. Replaces the default text.',
+ type: 'string',
+ status: 'optional',
+ },
+ skeleton: {
+ doc: 'Skeleton should be applied when loading content.',
+ type: 'boolean',
+ status: 'optional',
+ },
+ '[Space](/uilib/layout/space/properties)': {
+ doc: 'Spacing properties like `top` or `bottom` are supported.',
+ type: ['string', 'object'],
+ status: 'optional',
+ },
+}
+
+export const UploadEvents: PropertiesTableProps = {
+ onChange: {
+ doc: 'Will be called on `files` changes made by the user. Access the files with `{ files }` (containing each a `fileItem`).',
+ type: 'function',
+ status: 'optional',
+ },
+ onFileDelete: {
+ doc: 'Will be called once a file gets deleted by the user. Access the deleted file with `{ fileItem }`.',
+ type: 'function',
+ status: 'optional',
+ },
+}
diff --git a/packages/dnb-eufemia/src/components/upload/UploadDropzone.tsx b/packages/dnb-eufemia/src/components/upload/UploadDropzone.tsx
index 5ba391c2a18..35697bec15a 100644
--- a/packages/dnb-eufemia/src/components/upload/UploadDropzone.tsx
+++ b/packages/dnb-eufemia/src/components/upload/UploadDropzone.tsx
@@ -1,4 +1,10 @@
-import React from 'react'
+import React, {
+ useCallback,
+ useContext,
+ useEffect,
+ useRef,
+ useState,
+} from 'react'
import classnames from 'classnames'
import HeightAnimation from '../height-animation/HeightAnimation'
@@ -13,13 +19,13 @@ export default function UploadDropzone({
...rest
}: Partial) {
const props = rest as Omit
- const context = React.useContext(UploadContext)
- const [hover, setHover] = React.useState(false)
- const hoverTimeout = React.useRef()
+ const context = useContext(UploadContext)
+ const [hover, setHover] = useState(false)
+ const hoverTimeout = useRef()
const { onInputUpload, id } = context
- const getFiles = (event: UploadDragEvent) => {
+ const getFiles = useCallback((event: UploadDragEvent) => {
const fileData = event.dataTransfer
const files: UploadFile[] = []
@@ -29,35 +35,47 @@ export default function UploadDropzone({
})
return files
- }
+ }, [])
- const hoverHandler = (event: UploadDragEvent, state: boolean) => {
- event.stopPropagation()
- event.preventDefault()
- clearTimers()
- setHover(state)
- }
+ const clearTimers = useCallback(() => {
+ clearTimeout(hoverTimeout.current)
+ }, [])
- const dropHandler = (event: UploadDragEvent) => {
- const files = getFiles(event)
+ const hoverHandler = useCallback(
+ (event: UploadDragEvent, state: boolean) => {
+ event.stopPropagation()
+ event.preventDefault()
+ clearTimers()
+ setHover(state)
+ },
+ [clearTimers]
+ )
- onInputUpload(files)
- hoverHandler(event, false)
- }
+ const dropHandler = useCallback(
+ (event: UploadDragEvent) => {
+ const files = getFiles(event)
- const dragEnterHandler = (event: UploadDragEvent) => {
- hoverHandler(event, true)
- }
+ onInputUpload(files)
+ hoverHandler(event, false)
+ },
+ [getFiles, onInputUpload, hoverHandler]
+ )
- const dragLeaveHandler = (event: UploadDragEvent) => {
- hoverHandler(event, false)
- }
+ const dragEnterHandler = useCallback(
+ (event: UploadDragEvent) => {
+ hoverHandler(event, true)
+ },
+ [hoverHandler]
+ )
- const clearTimers = () => {
- clearTimeout(hoverTimeout.current)
- }
+ const dragLeaveHandler = useCallback(
+ (event: UploadDragEvent) => {
+ hoverHandler(event, false)
+ },
+ [hoverHandler]
+ )
- React.useEffect(() => {
+ useEffect(() => {
const elem = document.body
const execute = () => {
try {
@@ -89,7 +107,7 @@ export default function UploadDropzone({
//
}
}
- }, [])
+ }, [clearTimers, dragEnterHandler, dragLeaveHandler, dropHandler, id])
return (
{
fileSizeContent,
filesAmountLimit,
fileMaxSize,
+ children,
} = context
- const prettyfiedAcceptedFileFormats = acceptedFileTypes
+ const prettifiedAcceptedFileFormats = acceptedFileTypes
.join(', ')
.toUpperCase()
@@ -34,16 +35,18 @@ const UploadInfo = () => {
{text}
+ {children}
+
- {prettyfiedAcceptedFileFormats && (
+ {prettifiedAcceptedFileFormats && (
- {fileTypeDescription}
- - {prettyfiedAcceptedFileFormats}
+ - {prettifiedAcceptedFileFormats}
)}
diff --git a/packages/dnb-eufemia/src/components/upload/UploadVerify.tsx b/packages/dnb-eufemia/src/components/upload/UploadVerify.tsx
index a2aa9fbcdee..30c5a0cb588 100644
--- a/packages/dnb-eufemia/src/components/upload/UploadVerify.tsx
+++ b/packages/dnb-eufemia/src/components/upload/UploadVerify.tsx
@@ -5,7 +5,7 @@ import {
UploadAcceptedFileTypes,
} from './types'
-const BYTES_IN_A_MEGA_BYTE = 1048576
+export const BYTES_IN_A_MEGA_BYTE = 1048576
export function verifyFiles(
files: UploadFile[],
diff --git a/packages/dnb-eufemia/src/components/upload/types.ts b/packages/dnb-eufemia/src/components/upload/types.ts
index 88b0eaf7ebe..4e5ca32f59b 100644
--- a/packages/dnb-eufemia/src/components/upload/types.ts
+++ b/packages/dnb-eufemia/src/components/upload/types.ts
@@ -59,12 +59,13 @@ export type UploadProps = {
loadingText?: React.ReactNode
deleteButton?: React.ReactNode
fileListAriaLabel?: string
+ children?: React.ReactNode
}
export type UploadAllProps = UploadProps &
SpacingProps &
LocaleProps &
- Omit, 'onChange'>
+ Omit, 'onChange' | 'title'>
export type UploadContextProps = {
id?: string
diff --git a/packages/dnb-eufemia/src/components/upload/useUpload.ts b/packages/dnb-eufemia/src/components/upload/useUpload.ts
index 5107d3d7c39..03530988e1d 100644
--- a/packages/dnb-eufemia/src/components/upload/useUpload.ts
+++ b/packages/dnb-eufemia/src/components/upload/useUpload.ts
@@ -1,3 +1,4 @@
+import { useCallback, useMemo } from 'react'
import { useSharedState } from '../../shared/helpers/useSharedState'
import type { UploadFile } from './types'
@@ -18,29 +19,38 @@ function useUpload(id: string): useUploadReturn {
internalFiles?: UploadFile[]
}>(id)
- const setFiles = (files: UploadFile[]) => {
- extend({ files })
- }
+ const setFiles = useCallback(
+ (files: UploadFile[]) => {
+ extend({ files })
+ },
+ [extend]
+ )
- const setInternalFiles = (internalFiles: UploadFile[]) => {
- extend({ internalFiles })
- }
+ const setInternalFiles = useCallback(
+ (internalFiles: UploadFile[]) => {
+ extend({ internalFiles })
+ },
+ [extend]
+ )
- const files = data?.files || []
- const internalFiles = data?.internalFiles || []
-
- const getExistingFile = (
- file: File,
- fileItems: UploadFile[] = files
- ) => {
- return fileItems.find(({ file: f }) => {
- return (
- f.name === file.name &&
- f.size === file.size &&
- f.lastModified === file.lastModified
- )
- })
- }
+ const files = useMemo(() => data?.files || [], [data?.files])
+ const internalFiles = useMemo(
+ () => data?.internalFiles || [],
+ [data?.internalFiles]
+ )
+
+ const getExistingFile = useCallback(
+ (file: File, fileItems: UploadFile[] = files) => {
+ return fileItems.find(({ file: f }) => {
+ return (
+ f.name === file.name &&
+ f.size === file.size &&
+ f.lastModified === file.lastModified
+ )
+ })
+ },
+ [files]
+ )
return {
files,
diff --git a/packages/dnb-eufemia/src/extensions/forms/Field/ArraySelection/ArraySelection.tsx b/packages/dnb-eufemia/src/extensions/forms/Field/ArraySelection/ArraySelection.tsx
index 78a61373ed4..bcfd9bf998a 100644
--- a/packages/dnb-eufemia/src/extensions/forms/Field/ArraySelection/ArraySelection.tsx
+++ b/packages/dnb-eufemia/src/extensions/forms/Field/ArraySelection/ArraySelection.tsx
@@ -52,7 +52,7 @@ function ArraySelection(props: Props) {
children,
} = useFieldProps(props)
- const fieldSectionProps = {
+ const fieldBlockProps = {
forId: id,
className: classnames(
'dnb-forms-field-array-selection',
@@ -102,7 +102,7 @@ function ArraySelection(props: Props) {
switch (variant) {
case 'button':
return (
-
+
)
case 'checkbox':
- return {options}
+ return {options}
}
}
diff --git a/packages/dnb-eufemia/src/extensions/forms/Field/Composition/Composition.tsx b/packages/dnb-eufemia/src/extensions/forms/Field/Composition/Composition.tsx
index 8b4d9494013..ff65186e1c1 100644
--- a/packages/dnb-eufemia/src/extensions/forms/Field/Composition/Composition.tsx
+++ b/packages/dnb-eufemia/src/extensions/forms/Field/Composition/Composition.tsx
@@ -1,7 +1,7 @@
import React from 'react'
-import FieldBlock, { Props as FieldSectionProps } from '../../FieldBlock'
+import FieldBlock, { Props as FieldBlockProps } from '../../FieldBlock'
-function CompositionField(props: FieldSectionProps) {
+function CompositionField(props: FieldBlockProps) {
return
}
diff --git a/packages/dnb-eufemia/src/extensions/forms/Field/PhoneNumber/__tests__/PhoneNumber.test.tsx b/packages/dnb-eufemia/src/extensions/forms/Field/PhoneNumber/__tests__/PhoneNumber.test.tsx
index 4518c58232c..419cc26d59e 100644
--- a/packages/dnb-eufemia/src/extensions/forms/Field/PhoneNumber/__tests__/PhoneNumber.test.tsx
+++ b/packages/dnb-eufemia/src/extensions/forms/Field/PhoneNumber/__tests__/PhoneNumber.test.tsx
@@ -248,6 +248,8 @@ describe('Field.PhoneNumber', () => {
rerender()
+ const nordic = countries()
+
await userEvent.clear(phoneInput)
await userEvent.type(phoneInput, '123')
fireEvent.keyDown(ccInput, {
@@ -255,8 +257,6 @@ describe('Field.PhoneNumber', () => {
keyCode: 13,
})
- const nordic = countries()
-
expect(nordic).toHaveLength(7)
expect(nordic[0]).toHaveTextContent('+45 Danmark')
expect(nordic[1]).toHaveTextContent('+298 Færøyene')
@@ -268,6 +268,8 @@ describe('Field.PhoneNumber', () => {
rerender()
+ const europe = countries()
+
await userEvent.clear(phoneInput)
await userEvent.type(phoneInput, '123')
fireEvent.keyDown(ccInput, {
@@ -275,7 +277,6 @@ describe('Field.PhoneNumber', () => {
keyCode: 13,
})
- const europe = countries()
expect(europe).toHaveLength(51)
})
diff --git a/packages/dnb-eufemia/src/extensions/forms/Field/PostalCodeAndCity/PostalCodeAndCity.tsx b/packages/dnb-eufemia/src/extensions/forms/Field/PostalCodeAndCity/PostalCodeAndCity.tsx
index e8d7f1b0ea3..47db1c9db79 100644
--- a/packages/dnb-eufemia/src/extensions/forms/Field/PostalCodeAndCity/PostalCodeAndCity.tsx
+++ b/packages/dnb-eufemia/src/extensions/forms/Field/PostalCodeAndCity/PostalCodeAndCity.tsx
@@ -1,13 +1,13 @@
import React from 'react'
import classnames from 'classnames'
-import { Props as FieldSectionProps } from '../../FieldBlock'
+import { Props as FieldBlockProps } from '../../FieldBlock'
import StringField, { Props as StringFieldProps } from '../String'
import CompositionField from '../Composition'
import { FieldHelpProps } from '../../types'
import useTranslation from '../../hooks/useTranslation'
export type Props = FieldHelpProps &
- Omit &
+ Omit &
Partial>
function PostalCodeAndCity(props: Props) {
@@ -18,7 +18,7 @@ function PostalCodeAndCity(props: Props) {
city = {},
help,
width = 'large',
- ...fieldSectionProps
+ ...fieldBlockProps
} = props
return (
@@ -27,7 +27,7 @@ function PostalCodeAndCity(props: Props) {
'dnb-forms-field-postal-code-and-city',
props.className
)}
- {...fieldSectionProps}
+ {...fieldBlockProps}
width={width}
>
+
+
{variant === 'autocomplete' ? (
+
)
diff --git a/packages/dnb-eufemia/src/extensions/forms/Field/String/String.tsx b/packages/dnb-eufemia/src/extensions/forms/Field/String/String.tsx
index dcacadc4a59..61d24e3048c 100644
--- a/packages/dnb-eufemia/src/extensions/forms/Field/String/String.tsx
+++ b/packages/dnb-eufemia/src/extensions/forms/Field/String/String.tsx
@@ -252,7 +252,7 @@ function StringComponent(props: Props) {
keep_placeholder: keepPlaceholder,
}
- const fieldSectionProps = {
+ const fieldBlockProps = {
className: classnames('dnb-forms-field-string', className),
forId: id,
layout,
@@ -271,7 +271,7 @@ function StringComponent(props: Props) {
}
return (
-
+
{multiline ? (
) : mask ? (
diff --git a/packages/dnb-eufemia/src/extensions/forms/Field/Toggle/Toggle.tsx b/packages/dnb-eufemia/src/extensions/forms/Field/Toggle/Toggle.tsx
index 8a300e9c0b8..8f88a8f3f44 100644
--- a/packages/dnb-eufemia/src/extensions/forms/Field/Toggle/Toggle.tsx
+++ b/packages/dnb-eufemia/src/extensions/forms/Field/Toggle/Toggle.tsx
@@ -65,7 +65,7 @@ function Toggle(props: Props) {
const cn = classnames('dnb-forms-field-toggle', className)
- const fieldSectionPropsWithoutLabel = {
+ const fieldBlockPropsWithoutLabel = {
forId: id,
className: cn,
...pickSpacingProps(props),
@@ -75,8 +75,8 @@ function Toggle(props: Props) {
disabled,
}
- const fieldSectionProps = {
- ...fieldSectionPropsWithoutLabel,
+ const fieldBlockProps = {
+ ...fieldBlockPropsWithoutLabel,
layout,
label,
labelDescription,
@@ -94,7 +94,7 @@ function Toggle(props: Props) {
default:
case 'checkbox':
return (
-
+
+
@@ -178,7 +178,7 @@ function Toggle(props: Props) {
)
case 'checkbox-button':
return (
-
+
+export type Props = FieldHelpProps &
+ Omit, 'name'> &
+ SpacingProps & {
+ width?: Omit
+ } & Pick<
+ Partial,
+ | 'title'
+ | 'text'
+ | 'acceptedFileTypes'
+ | 'filesAmountLimit'
+ | 'fileMaxSize'
+ | 'onFileDelete'
+ | 'skeleton'
+ >
+
+function UploadComponent(props: Props) {
+ const validateRequired = useCallback(
+ (value: UploadValue, { required, isChanged, error }) => {
+ const hasError = value?.some((file) => file.errorMessage)
+ if (hasError) {
+ return new FormError(error.message, {
+ validationRule: 'invalid',
+ })
+ }
+
+ if (required && (!isChanged || !(value.length > 0))) {
+ return error
+ }
+
+ return undefined
+ },
+ []
+ )
+
+ const sharedTr = useSharedTranslation().Upload
+ const formsTr = useFormsTranslation().Upload
+
+ const preparedProps = {
+ errorMessages: {
+ required: formsTr.errorRequired,
+ invalid: formsTr.errorInvalidFiles,
+ },
+ validateRequired,
+ ...props,
+ }
+
+ const {
+ id,
+ className,
+ width: widthProp = 'stretch',
+ layout,
+ value,
+ label,
+ labelDescription,
+ disabled,
+ help,
+ info,
+ warning,
+ error,
+ htmlAttributes,
+ handleChange,
+ handleFocus,
+ handleBlur,
+ ...rest
+ } = useFieldProps(preparedProps, {
+ executeOnChangeRegardlessOfError: true,
+ })
+
+ // Upload props
+ const {
+ title = sharedTr.title,
+ text = sharedTr.text,
+ acceptedFileTypes = ['pdf', 'png', 'jpg', 'jpeg'],
+ filesAmountLimit = 100,
+ fileMaxSize = 5,
+ skeleton,
+ onFileDelete,
+ } = rest
+
+ const { setFiles } = useUpload(id)
+
+ useEffect(() => {
+ setFiles(value)
+ }, [handleBlur, setFiles, value])
+
+ const changeHandler = useCallback(
+ ({ files }: { files: UploadValue }) => {
+ // Prevents the form-status from showing up
+ handleBlur()
+ handleFocus()
+ handleChange(files)
+ },
+ [handleBlur, handleChange, handleFocus]
+ )
+
+ const width = widthProp as FieldBlockWidth
+ const fieldBlockProps: FieldBlockProps = {
+ forId: id,
+ layout,
+ label,
+ labelSrOnly: true,
+ info,
+ warning,
+ error,
+ className,
+ disabled,
+ width,
+ ...pickSpacingProps(props),
+ }
+
+ return (
+
+
+ {labelDescription ?? text}
+
+ {help.content}
+
+ >
+ ) : (
+ labelDescription ?? text
+ )
+ }
+ {...htmlAttributes}
+ />
+
+ )
+}
+
+export default UploadComponent
+
+UploadComponent._supportsSpacingProps = true
diff --git a/packages/dnb-eufemia/src/extensions/forms/Field/Upload/UploadDocs.ts b/packages/dnb-eufemia/src/extensions/forms/Field/Upload/UploadDocs.ts
new file mode 100644
index 00000000000..0fdfe4b5949
--- /dev/null
+++ b/packages/dnb-eufemia/src/extensions/forms/Field/Upload/UploadDocs.ts
@@ -0,0 +1,15 @@
+import {
+ UploadEvents,
+ UploadProperties,
+} from '../../../../components/upload/UploadDocs'
+import { PropertiesTableProps } from '../../../../shared/types'
+
+export const UploadFieldProperties: PropertiesTableProps = {
+ ...UploadProperties,
+ title: undefined,
+ text: undefined,
+}
+
+export const UploadFieldEvents: PropertiesTableProps = {
+ ...UploadEvents,
+}
diff --git a/packages/dnb-eufemia/src/extensions/forms/Field/Upload/__tests__/Upload.screenshot.test.ts b/packages/dnb-eufemia/src/extensions/forms/Field/Upload/__tests__/Upload.screenshot.test.ts
new file mode 100644
index 00000000000..c559e294dca
--- /dev/null
+++ b/packages/dnb-eufemia/src/extensions/forms/Field/Upload/__tests__/Upload.screenshot.test.ts
@@ -0,0 +1,17 @@
+import {
+ makeScreenshot,
+ setupPageScreenshot,
+} from '../../../../../core/jest/jestSetupScreenshots'
+
+describe('Upload', () => {
+ setupPageScreenshot({
+ url: '/uilib/extensions/forms/feature-fields/Upload/demos',
+ })
+
+ it('have to match upload-field-customized', async () => {
+ const screenshot = await makeScreenshot({
+ selector: '[data-visual-test="upload-field-customized"]',
+ })
+ expect(screenshot).toMatchImageSnapshot()
+ })
+})
diff --git a/packages/dnb-eufemia/src/extensions/forms/Field/Upload/__tests__/Upload.test.tsx b/packages/dnb-eufemia/src/extensions/forms/Field/Upload/__tests__/Upload.test.tsx
new file mode 100644
index 00000000000..cf642fef1b1
--- /dev/null
+++ b/packages/dnb-eufemia/src/extensions/forms/Field/Upload/__tests__/Upload.test.tsx
@@ -0,0 +1,740 @@
+import React from 'react'
+import { fireEvent, render, waitFor, screen } from '@testing-library/react'
+import { Field, Form } from '../../..'
+import { BYTES_IN_A_MEGA_BYTE } from '../../../../../components/upload/UploadVerify'
+import { createMockFile } from '../../../../../components/upload/__tests__/testHelpers'
+
+import nbNOForms from '../../../constants/locales/nb-NO'
+import nbNOShared from '../../../../../shared/locales/nb-NO'
+import userEvent from '@testing-library/user-event'
+
+const nbForms = nbNOForms['nb-NO']
+const nbShared = nbNOShared['nb-NO']
+
+global.URL.createObjectURL = jest.fn(() => 'url')
+
+describe('Field.Upload', () => {
+ const getRootElement = () => document.querySelector('.dnb-upload')
+
+ it('should render with defaults', () => {
+ render()
+
+ const [title, text] = Array.from(document.querySelectorAll('p'))
+ expect(title).toHaveTextContent(nbShared.Upload.title)
+ expect(text).toHaveTextContent(nbShared.Upload.text)
+
+ const [firstDt, secondDt] = Array.from(
+ document.querySelectorAll('dl dt')
+ )
+ expect(firstDt).toHaveTextContent(nbShared.Upload.fileTypeDescription)
+ expect(secondDt).toHaveTextContent(nbShared.Upload.fileSizeDescription)
+
+ const [firstDd, , thirdDd, , fourthDd] = Array.from(
+ document.querySelectorAll('dl dd')
+ )
+ expect(firstDd).toHaveTextContent('PDF')
+ expect(thirdDd).toHaveTextContent('5 MB')
+ expect(fourthDd).toBeUndefined()
+ })
+
+ it('should render with custom properties', () => {
+ render(
+
+ )
+
+ const [firstDt, secondDt, thirdDt] = Array.from(
+ document.querySelectorAll('dl dt')
+ )
+ expect(firstDt).toHaveTextContent(nbShared.Upload.fileTypeDescription)
+ expect(secondDt).toHaveTextContent(nbShared.Upload.fileSizeDescription)
+ expect(thirdDt).toHaveTextContent(
+ nbShared.Upload.fileAmountDescription
+ )
+
+ const [firstDd, , thirdDd, , fourthDd] = Array.from(
+ document.querySelectorAll('dl dd')
+ )
+ expect(firstDd).toHaveTextContent('PDF')
+ expect(thirdDd).toHaveTextContent('1 MB')
+ expect(fourthDd).toHaveTextContent('2')
+ })
+
+ it('should render label and text', () => {
+ render()
+
+ const [title, text] = Array.from(document.querySelectorAll('p'))
+ expect(title).toHaveTextContent('My Label')
+ expect(text).toHaveTextContent('My Text')
+ })
+
+ it('should render files given in data context', () => {
+ render(
+ console.log('onChange', data)}
+ data={{
+ myFiles: [
+ { file: createMockFile('fileName-1.png', 100, 'image/png') },
+ ],
+ }}
+ >
+
+
+ )
+
+ const list = document.querySelector('ul')
+ expect(list).toHaveTextContent('fileName-1.png')
+ })
+
+ it('should render predefined value', () => {
+ render(
+
+ )
+
+ const list = document.querySelector('ul')
+ expect(list).toHaveTextContent('fileName-1.png')
+ })
+
+ it('should render help prop', () => {
+ render(
+
+ )
+
+ const helpButton = document.querySelector('.dnb-help-button')
+ expect(helpButton).toBeInTheDocument()
+ })
+
+ describe('sync validation', () => {
+ it('should emit data context based on required-prop', async () => {
+ const onSubmit = jest.fn()
+
+ render(
+
+
+
+
+ )
+
+ fireEvent.submit(document.querySelector('form'))
+
+ expect(onSubmit).toHaveBeenCalledTimes(1)
+ expect(onSubmit).toHaveBeenCalledWith(
+ { myFiles: undefined },
+ expect.anything()
+ )
+
+ const element = getRootElement()
+
+ const file1 = createMockFile('fileName-1.png', 100, 'image/png')
+ const file2 = createMockFile('fileName-2.png', 100, 'image/png')
+
+ await waitFor(() =>
+ fireEvent.drop(element, {
+ dataTransfer: { files: [file1, file2] },
+ })
+ )
+
+ await waitFor(() =>
+ fireEvent.drop(element, {
+ dataTransfer: { files: [file2, file2] },
+ })
+ )
+
+ fireEvent.submit(document.querySelector('form'))
+
+ expect(onSubmit).toHaveBeenCalledTimes(2)
+ expect(onSubmit).toHaveBeenLastCalledWith(
+ {
+ myFiles: [
+ {
+ file: file1,
+ id: expect.any(String),
+ exists: expect.any(Boolean),
+ },
+ {
+ file: file2,
+ id: expect.any(String),
+ exists: expect.any(Boolean),
+ },
+ ],
+ },
+ expect.anything()
+ )
+ })
+
+ it('should prevent submit when error in one file is present', async () => {
+ const onChangeContext = jest.fn()
+ const onChangeField = jest.fn()
+ const onSubmit = jest.fn()
+
+ render(
+
+
+
+
+ )
+
+ const getRootElement = () => document.querySelector('.dnb-upload')
+
+ const element = getRootElement()
+
+ const file1 = createMockFile('fileName-1.png', 100, 'image/png')
+ const file2 = createMockFile(
+ 'fileName-2.png',
+ 5 * BYTES_IN_A_MEGA_BYTE + 1, // exceeds the default fileMaxSize
+ 'image/png'
+ )
+
+ await waitFor(() =>
+ fireEvent.drop(element, {
+ dataTransfer: { files: [file1, file2] },
+ })
+ )
+
+ expect(onChangeContext).toHaveBeenCalledTimes(1)
+ expect(onChangeContext).toHaveBeenLastCalledWith({
+ myFiles: [
+ {
+ file: file1,
+ exists: false,
+ id: expect.anything(),
+ },
+ {
+ errorMessage: nbShared.Upload.errorLargeFile.replace(
+ '%size',
+ '5'
+ ),
+ file: file2,
+ exists: false,
+ id: expect.anything(),
+ },
+ ],
+ })
+ expect(onChangeField).toHaveBeenCalledTimes(1)
+ expect(onChangeField).toHaveBeenLastCalledWith([
+ {
+ file: file1,
+ exists: false,
+ id: expect.anything(),
+ },
+ {
+ errorMessage: nbShared.Upload.errorLargeFile.replace(
+ '%size',
+ '5'
+ ),
+ file: file2,
+ exists: false,
+ id: expect.anything(),
+ },
+ ])
+
+ fireEvent.submit(document.querySelector('form'))
+
+ expect(
+ document.querySelector(
+ '.dnb-forms-field-block__status .dnb-form-status'
+ )
+ ).toHaveTextContent(nbForms.Upload.errorInvalidFiles)
+
+ const deleteButton = screen.queryAllByRole('button', {
+ name: nbShared.Upload.deleteButton,
+ })
+ fireEvent.click(deleteButton[1])
+ fireEvent.submit(document.querySelector('form'))
+
+ expect(onSubmit).toHaveBeenCalledTimes(1)
+ expect(onSubmit).toHaveBeenLastCalledWith(
+ {
+ myFiles: [
+ {
+ file: file1,
+ exists: false,
+ id: expect.anything(),
+ },
+ ],
+ },
+ expect.anything()
+ )
+ })
+
+ it('should handle "required" logic based on, if files are present', async () => {
+ const onChange = jest.fn((args) => args)
+ const onSubmit = jest.fn((args) => args)
+
+ render(
+
+
+
+
+ )
+
+ fireEvent.submit(document.querySelector('form'))
+
+ expect(document.querySelector('.dnb-form-status')).toHaveTextContent(
+ nbForms.Upload.errorRequired
+ )
+
+ const element = getRootElement()
+ const file1 = createMockFile('fileName-1.png', 100, 'image/png')
+
+ await waitFor(() =>
+ fireEvent.drop(element, {
+ dataTransfer: {
+ files: [file1],
+ },
+ })
+ )
+
+ expect(onChange).toHaveBeenCalledTimes(1)
+ expect(onChange).toHaveBeenLastCalledWith({
+ myFiles: [
+ expect.objectContaining({
+ exists: false,
+ file: file1,
+ id: expect.any(String),
+ }),
+ ],
+ })
+
+ expect(
+ document.querySelector('.dnb-form-status')
+ ).not.toBeInTheDocument()
+
+ const deleteButton = screen.queryByRole('button', {
+ name: nbShared.Upload.deleteButton,
+ })
+
+ fireEvent.click(deleteButton)
+
+ expect(
+ document.querySelector('.dnb-form-status')
+ ).not.toBeInTheDocument()
+
+ fireEvent.submit(document.querySelector('form'))
+
+ expect(document.querySelector('.dnb-form-status')).toHaveTextContent(
+ nbForms.Upload.errorRequired
+ )
+
+ expect(onChange).toHaveBeenCalledTimes(2)
+ expect(onSubmit).toHaveBeenCalledTimes(0)
+
+ await waitFor(() =>
+ fireEvent.drop(element, {
+ dataTransfer: {
+ files: [file1],
+ },
+ })
+ )
+ fireEvent.submit(document.querySelector('form'))
+
+ expect(onChange).toHaveBeenCalledTimes(3)
+ expect(onChange).toHaveBeenLastCalledWith({
+ myFiles: [
+ expect.objectContaining({
+ exists: false,
+ file: file1,
+ id: expect.any(String),
+ }),
+ ],
+ })
+ expect(onSubmit).toHaveBeenCalledTimes(1)
+ expect(onSubmit).toHaveBeenLastCalledWith(
+ {
+ myFiles: [
+ expect.objectContaining({
+ exists: false,
+ file: file1,
+ id: expect.any(String),
+ }),
+ ],
+ },
+ {
+ clearData: expect.any(Function),
+ resetForm: expect.any(Function),
+ }
+ )
+ })
+
+ it('should handle validation based on files with error', async () => {
+ const onChange = jest.fn((args) => args)
+ const onSubmit = jest.fn((args) => args)
+
+ render(
+
+
+
+ )
+
+ fireEvent.submit(document.querySelector('form'))
+
+ expect(document.querySelector('.dnb-form-status')).toHaveTextContent(
+ nbForms.Upload.errorRequired
+ )
+
+ const element = getRootElement()
+ const file1 = createMockFile(
+ 'fileName-1.png',
+ 0.2 * BYTES_IN_A_MEGA_BYTE + 1,
+ 'image/png'
+ )
+
+ await waitFor(() =>
+ fireEvent.drop(element, {
+ dataTransfer: {
+ files: [file1],
+ },
+ })
+ )
+
+ expect(onChange).toHaveBeenCalledTimes(1)
+ expect(onChange).toHaveBeenLastCalledWith({
+ myFiles: [
+ {
+ errorMessage: nbShared.Upload.errorLargeFile.replace(
+ '%size',
+ '0,2'
+ ),
+ file: file1,
+ exists: false,
+ id: expect.anything(),
+ },
+ ],
+ })
+
+ expect(
+ document.querySelector(
+ '.dnb-forms-field-block__status .dnb-form-status'
+ )
+ ).not.toBeInTheDocument()
+
+ fireEvent.submit(document.querySelector('form'))
+
+ expect(
+ document.querySelector(
+ '.dnb-forms-field-block__status .dnb-form-status'
+ )
+ ).toHaveTextContent(nbForms.Upload.errorInvalidFiles)
+
+ const deleteButton = screen.queryByRole('button', {
+ name: nbShared.Upload.deleteButton,
+ })
+
+ fireEvent.click(deleteButton)
+
+ expect(
+ document.querySelector(
+ '.dnb-forms-field-block__status .dnb-form-status'
+ )
+ ).not.toBeInTheDocument()
+
+ fireEvent.submit(document.querySelector('form'))
+
+ expect(onChange).toHaveBeenCalledTimes(2)
+ expect(onChange).toHaveBeenLastCalledWith({ myFiles: [] })
+
+ expect(
+ document.querySelector(
+ '.dnb-forms-field-block__status .dnb-form-status'
+ )
+ ).toHaveTextContent(nbForms.Upload.errorRequired)
+
+ const file2 = createMockFile('fileName-1.png', 100, 'image/png')
+
+ await waitFor(() =>
+ fireEvent.drop(element, {
+ dataTransfer: {
+ files: [file2],
+ },
+ })
+ )
+ fireEvent.submit(document.querySelector('form'))
+
+ expect(onChange).toHaveBeenCalledTimes(3)
+ expect(onChange).toHaveBeenLastCalledWith({
+ myFiles: [
+ expect.objectContaining({
+ exists: false,
+ file: file2,
+ id: expect.any(String),
+ }),
+ ],
+ })
+ expect(onSubmit).toHaveBeenCalledTimes(1)
+ expect(onSubmit).toHaveBeenLastCalledWith(
+ {
+ myFiles: [
+ expect.objectContaining({
+ exists: false,
+ file: file2,
+ id: expect.any(String),
+ }),
+ ],
+ },
+ {
+ clearData: expect.any(Function),
+ resetForm: expect.any(Function),
+ }
+ )
+ })
+ })
+
+ describe('async validation', () => {
+ it('should handle "required" logic based on, if files are present', async () => {
+ const onChange = jest.fn(async (args) => args)
+ const onSubmit = jest.fn(async (args) => args)
+
+ render(
+
+
+
+
+ )
+
+ const submitButton = document.querySelector('button[type="submit"]')
+ await userEvent.click(submitButton)
+
+ expect(
+ document.querySelector(
+ '.dnb-forms-field-block__status .dnb-form-status'
+ )
+ ).toHaveTextContent(nbForms.Upload.errorRequired)
+
+ const element = getRootElement()
+ const file1 = createMockFile('fileName-1.png', 100, 'image/png')
+
+ await waitFor(() =>
+ fireEvent.drop(element, {
+ dataTransfer: {
+ files: [file1],
+ },
+ })
+ )
+
+ expect(onChange).toHaveBeenCalledTimes(1)
+ expect(onChange).toHaveBeenLastCalledWith({
+ myFiles: [
+ expect.objectContaining({
+ exists: false,
+ file: file1,
+ id: expect.any(String),
+ }),
+ ],
+ })
+
+ expect(
+ document.querySelector(
+ '.dnb-forms-field-block__status .dnb-form-status'
+ )
+ ).not.toBeInTheDocument()
+
+ const deleteButton = screen.queryByRole('button', {
+ name: nbShared.Upload.deleteButton,
+ })
+
+ fireEvent.click(deleteButton)
+
+ expect(
+ document.querySelector(
+ '.dnb-forms-field-block__status .dnb-form-status'
+ )
+ ).not.toBeInTheDocument()
+
+ await userEvent.click(submitButton)
+
+ expect(
+ document.querySelector(
+ '.dnb-forms-field-block__status .dnb-form-status'
+ )
+ ).toHaveTextContent(nbForms.Upload.errorRequired)
+
+ expect(onChange).toHaveBeenCalledTimes(2)
+ expect(onSubmit).toHaveBeenCalledTimes(0)
+
+ await waitFor(() =>
+ fireEvent.drop(element, {
+ dataTransfer: {
+ files: [file1],
+ },
+ })
+ )
+ await userEvent.click(submitButton)
+
+ expect(onChange).toHaveBeenCalledTimes(3)
+ expect(onChange).toHaveBeenLastCalledWith({
+ myFiles: [
+ expect.objectContaining({
+ exists: false,
+ file: file1,
+ id: expect.any(String),
+ }),
+ ],
+ })
+ expect(onSubmit).toHaveBeenCalledTimes(1)
+ expect(onSubmit).toHaveBeenLastCalledWith(
+ {
+ myFiles: [
+ expect.objectContaining({
+ exists: false,
+ file: file1,
+ id: expect.any(String),
+ }),
+ ],
+ },
+ {
+ clearData: expect.any(Function),
+ resetForm: expect.any(Function),
+ }
+ )
+ })
+
+ it('should handle validation based on files with error', async () => {
+ const onChange = jest.fn(async (args) => args)
+ const onSubmit = jest.fn(async (args) => args)
+
+ render(
+
+
+
+
+ )
+
+ const submitButton = document.querySelector('button[type="submit"]')
+ await userEvent.click(submitButton)
+
+ expect(
+ document.querySelector(
+ '.dnb-forms-field-block__status .dnb-form-status'
+ )
+ ).toHaveTextContent(nbForms.Upload.errorRequired)
+
+ const element = getRootElement()
+ const file1 = createMockFile(
+ 'fileName-1.png',
+ 0.2 * BYTES_IN_A_MEGA_BYTE + 1,
+ 'image/png'
+ )
+
+ await waitFor(() =>
+ fireEvent.drop(element, {
+ dataTransfer: {
+ files: [file1],
+ },
+ })
+ )
+
+ expect(onChange).toHaveBeenCalledTimes(1)
+ expect(onChange).toHaveBeenLastCalledWith({
+ myFiles: [
+ {
+ errorMessage: nbShared.Upload.errorLargeFile.replace(
+ '%size',
+ '0,2'
+ ),
+ file: file1,
+ exists: false,
+ id: expect.anything(),
+ },
+ ],
+ })
+
+ expect(
+ document.querySelector(
+ '.dnb-forms-field-block__status .dnb-form-status'
+ )
+ ).not.toBeInTheDocument()
+
+ await userEvent.click(submitButton)
+
+ expect(
+ document.querySelector(
+ '.dnb-forms-field-block__status .dnb-form-status'
+ )
+ ).toHaveTextContent(nbForms.Upload.errorInvalidFiles)
+
+ const deleteButton = screen.queryByRole('button', {
+ name: nbShared.Upload.deleteButton,
+ })
+
+ fireEvent.click(deleteButton)
+
+ expect(
+ document.querySelector(
+ '.dnb-forms-field-block__status .dnb-form-status'
+ )
+ ).not.toBeInTheDocument()
+
+ await userEvent.click(submitButton)
+
+ expect(onChange).toHaveBeenCalledTimes(2)
+ expect(onChange).toHaveBeenLastCalledWith({
+ myFiles: [],
+ })
+
+ expect(
+ document.querySelector(
+ '.dnb-forms-field-block__status .dnb-form-status'
+ )
+ ).toHaveTextContent(nbForms.Upload.errorRequired)
+
+ const file2 = createMockFile('fileName-1.png', 100, 'image/png')
+
+ await waitFor(() =>
+ fireEvent.drop(element, {
+ dataTransfer: {
+ files: [file2],
+ },
+ })
+ )
+ await userEvent.click(submitButton)
+
+ expect(onChange).toHaveBeenCalledTimes(3)
+ expect(onChange).toHaveBeenLastCalledWith({
+ myFiles: [
+ expect.objectContaining({
+ exists: false,
+ file: file2,
+ id: expect.any(String),
+ }),
+ ],
+ })
+ expect(onSubmit).toHaveBeenCalledTimes(1)
+ expect(onSubmit).toHaveBeenLastCalledWith(
+ {
+ myFiles: [
+ expect.objectContaining({
+ exists: false,
+ file: file2,
+ id: expect.any(String),
+ }),
+ ],
+ },
+ {
+ clearData: expect.any(Function),
+ resetForm: expect.any(Function),
+ }
+ )
+ })
+ })
+})
diff --git a/packages/dnb-eufemia/src/extensions/forms/Field/Upload/__tests__/__image_snapshots__/upload-have-to-match-upload-field-customized.snap.png b/packages/dnb-eufemia/src/extensions/forms/Field/Upload/__tests__/__image_snapshots__/upload-have-to-match-upload-field-customized.snap.png
new file mode 100644
index 00000000000..b60a1be5d67
Binary files /dev/null and b/packages/dnb-eufemia/src/extensions/forms/Field/Upload/__tests__/__image_snapshots__/upload-have-to-match-upload-field-customized.snap.png differ
diff --git a/packages/dnb-eufemia/src/extensions/forms/Field/Upload/index.ts b/packages/dnb-eufemia/src/extensions/forms/Field/Upload/index.ts
new file mode 100644
index 00000000000..c2369c59991
--- /dev/null
+++ b/packages/dnb-eufemia/src/extensions/forms/Field/Upload/index.ts
@@ -0,0 +1,2 @@
+export { default } from './Upload'
+export * from './Upload'
diff --git a/packages/dnb-eufemia/src/extensions/forms/Field/Upload/stories/Upload.stories.tsx b/packages/dnb-eufemia/src/extensions/forms/Field/Upload/stories/Upload.stories.tsx
new file mode 100644
index 00000000000..72902c637b2
--- /dev/null
+++ b/packages/dnb-eufemia/src/extensions/forms/Field/Upload/stories/Upload.stories.tsx
@@ -0,0 +1,45 @@
+import { Field, Form } from '../../..'
+import { Flex } from '../../../../../components'
+// import { createMockFile } from '../../../../../components/upload/__tests__/testHelpers'
+
+export default {
+ title: 'Eufemia/Extensions/Forms/Upload',
+}
+
+export function Upload() {
+ // const { setFiles } = OriginalUpload.useUpload('unique-id')
+
+ // React.useEffect(() => {
+ // setFiles([
+ // { file: createMockFile('fileName-1.png', 100, 'image/png') },
+ // ])
+ // }, [setFiles])
+
+ return (
+ {
+ console.log('global onChange', data)
+ }}
+ onSubmit={(data) => console.log('global onSubmit', data)}
+ >
+
+ {
+ console.log('local onChange', e)
+ }}
+ />
+
+
+
+
+ )
+}
diff --git a/packages/dnb-eufemia/src/extensions/forms/Field/index.ts b/packages/dnb-eufemia/src/extensions/forms/Field/index.ts
index 0147f193eca..2190e5f122a 100644
--- a/packages/dnb-eufemia/src/extensions/forms/Field/index.ts
+++ b/packages/dnb-eufemia/src/extensions/forms/Field/index.ts
@@ -20,3 +20,4 @@ export { default as BankAccountNumber } from './BankAccountNumber'
export { default as Expiry } from './Expiry'
export { default as Password } from './Password'
export { default as Slider } from './Slider'
+export { default as Upload } from './Upload'
diff --git a/packages/dnb-eufemia/src/extensions/forms/FieldBlock/FieldBlock.tsx b/packages/dnb-eufemia/src/extensions/forms/FieldBlock/FieldBlock.tsx
index d1c0294e005..6a778417a2b 100644
--- a/packages/dnb-eufemia/src/extensions/forms/FieldBlock/FieldBlock.tsx
+++ b/packages/dnb-eufemia/src/extensions/forms/FieldBlock/FieldBlock.tsx
@@ -61,6 +61,8 @@ export type Props = Pick<
forId?: string
/** Use true if you have more than one form element */
asFieldset?: boolean
+ /** use `true` to make the label only readable by screen readers. */
+ labelSrOnly?: boolean
/** Defines the layout of nested fields */
composition?: FieldBlockContextProps['composition']
/** Width of outer block element */
@@ -82,7 +84,9 @@ function FieldBlock(props: Props) {
const dataContext = useContext(DataContext)
const nestedFieldBlockContext = useContext(FieldBlockContext)
- const sharedData = createSharedState(props.forId || props.id)
+ const sharedData = createSharedState(
+ 'field-block-props-' + (props.forId || props.id)
+ )
const {
className,
forId,
@@ -90,6 +94,7 @@ function FieldBlock(props: Props) {
composition,
label: labelProp,
labelDescription,
+ labelSrOnly,
asFieldset,
info,
warning,
@@ -376,6 +381,7 @@ function FieldBlock(props: Props) {
const labelProps: FormLabelAllProps = {
element: enableFieldset ? 'legend' : 'label',
forId: enableFieldset ? undefined : forId,
+ srOnly: labelSrOnly,
space: { top: 0, bottom: 'x-small' },
size: labelSize,
disabled,
diff --git a/packages/dnb-eufemia/src/extensions/forms/FieldBlock/FieldBlockDocs.ts b/packages/dnb-eufemia/src/extensions/forms/FieldBlock/FieldBlockDocs.ts
index 568388f18cc..407be2df08d 100644
--- a/packages/dnb-eufemia/src/extensions/forms/FieldBlock/FieldBlockDocs.ts
+++ b/packages/dnb-eufemia/src/extensions/forms/FieldBlock/FieldBlockDocs.ts
@@ -11,6 +11,11 @@ export const fieldBlockSharedProperties: PropertiesTableProps = {
type: 'string',
status: 'optional',
},
+ labelSrOnly: {
+ doc: 'Use `true` to make the label only readable by screen readers.',
+ type: 'boolean',
+ status: 'optional',
+ },
layout: {
doc: 'Layout for the label and input. Can be `horizontal` or `vertical`.',
type: 'string',
diff --git a/packages/dnb-eufemia/src/extensions/forms/Form/Section/Section.tsx b/packages/dnb-eufemia/src/extensions/forms/Form/Section/Section.tsx
index 3d9e6f9703b..cb759139329 100644
--- a/packages/dnb-eufemia/src/extensions/forms/Form/Section/Section.tsx
+++ b/packages/dnb-eufemia/src/extensions/forms/Form/Section/Section.tsx
@@ -10,14 +10,14 @@ import EditContainer from './EditContainer'
import type { Props as DataContextProps } from '../../DataContext/Provider'
import type { ContainerMode } from './containers/SectionContainer'
import type {
- FieldSectionProps,
+ FieldBlockProps,
Path,
FieldProps,
OnChange,
} from '../../types'
export type OverwritePropsDefaults = {
- [key: Path]: (FieldProps & FieldSectionProps) | OverwritePropsDefaults
+ [key: Path]: (FieldProps & FieldBlockProps) | OverwritePropsDefaults
}
export type SectionProps = {
/**
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 dbdba32e619..431ace38d29 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
@@ -150,5 +150,9 @@ export default {
ariaLabelShow: 'Show password',
ariaLabelHide: 'Hide password',
},
+ Upload: {
+ errorRequired: 'You must upload a file.',
+ errorInvalidFiles: 'Remove all files that have errors.',
+ },
},
}
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 2baad28afdf..4f8a4aa63c5 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
@@ -149,5 +149,9 @@ export default {
ariaLabelShow: 'Vis passord',
ariaLabelHide: 'Skjul passord',
},
+ Upload: {
+ errorRequired: 'Du må laste opp minst en fil.',
+ errorInvalidFiles: 'Fjern alle filer som har feil.',
+ },
},
}
diff --git a/packages/dnb-eufemia/src/extensions/forms/hooks/__tests__/useFieldProps.test.tsx b/packages/dnb-eufemia/src/extensions/forms/hooks/__tests__/useFieldProps.test.tsx
index 83fd166301e..0aabd97d503 100644
--- a/packages/dnb-eufemia/src/extensions/forms/hooks/__tests__/useFieldProps.test.tsx
+++ b/packages/dnb-eufemia/src/extensions/forms/hooks/__tests__/useFieldProps.test.tsx
@@ -353,7 +353,7 @@ describe('useFieldProps', () => {
fieldState: SubmitState
info: string
warning: string
- }>(id)
+ }>('field-block-props-' + id)
)
expect(sharedResult.current.data).toEqual({
disabled: undefined,
@@ -2640,4 +2640,79 @@ describe('useFieldProps', () => {
expect.objectContaining(htmlAttributes)
)
})
+
+ it('should forward props in a props object', () => {
+ const props = {
+ foo: 'bar',
+ } as Record
+
+ const { result } = renderHook(() => useFieldProps(props))
+
+ expect(result.current.props).toEqual(expect.objectContaining(props))
+ })
+
+ it('should call async context onChange when no error is present', async () => {
+ const onChange = jest.fn(async () => null)
+
+ const { result } = renderHook(useFieldProps, {
+ initialProps: {
+ path: '/foo',
+ error: undefined,
+ },
+ wrapper: (props) => ,
+ })
+
+ await act(async () => {
+ result.current.handleChange('new-value')
+ })
+
+ expect(onChange).toHaveBeenCalledTimes(1)
+ expect(onChange).toHaveBeenLastCalledWith({ foo: 'new-value' })
+ expect(result.current.error).toBeUndefined()
+ })
+
+ it('should not call async context onChange when error is present', async () => {
+ const onChange = jest.fn(async () => null)
+
+ const { result } = renderHook(useFieldProps, {
+ initialProps: {
+ path: '/foo',
+ error: new Error('Error message'),
+ },
+ wrapper: (props) => ,
+ })
+
+ await act(async () => {
+ result.current.handleChange('new-value')
+ })
+
+ expect(onChange).toHaveBeenCalledTimes(0)
+ expect(result.current.error).toBeInstanceOf(Error)
+ })
+
+ it('should call async context onChange regardless of error when executeOnChangeRegardlessOfError is true', async () => {
+ const onChange = jest.fn(async () => null)
+
+ const { result } = renderHook(
+ (props) =>
+ useFieldProps(props, {
+ executeOnChangeRegardlessOfError: true,
+ }),
+ {
+ initialProps: {
+ path: '/foo',
+ error: new Error('Error message'),
+ },
+ wrapper: (props) => ,
+ }
+ )
+
+ await act(async () => {
+ result.current.handleChange('new-value')
+ })
+
+ expect(onChange).toHaveBeenCalledTimes(1)
+ expect(onChange).toHaveBeenLastCalledWith({ foo: 'new-value' })
+ expect(result.current.error).toBeInstanceOf(Error)
+ })
})
diff --git a/packages/dnb-eufemia/src/extensions/forms/hooks/__tests__/useTranslation.test.tsx b/packages/dnb-eufemia/src/extensions/forms/hooks/__tests__/useTranslation.test.tsx
index 116c895ed89..0495c808421 100644
--- a/packages/dnb-eufemia/src/extensions/forms/hooks/__tests__/useTranslation.test.tsx
+++ b/packages/dnb-eufemia/src/extensions/forms/hooks/__tests__/useTranslation.test.tsx
@@ -7,13 +7,13 @@ import React from 'react'
import { renderHook } from '@testing-library/react'
import useTranslation from '../useTranslation'
import Provider from '../../../../shared/Provider'
-import { LOCALE as defaultLocale } from '../../../../shared/defaults'
// Translations
import forms_nbNO from '../../constants/locales/nb-NO'
import forms_enGB from '../../constants/locales/en-GB'
import global_nbNO from '../../../../shared/locales/nb-NO'
import global_enGB from '../../../../shared/locales/en-GB'
+import { extendDeep } from '../../../../shared/component-helper'
describe('Form.useTranslation', () => {
it('should default to nb-NO if no locale is specified in context', () => {
@@ -21,15 +21,11 @@ describe('Form.useTranslation', () => {
wrapper: ({ children }) => {children},
})
- expect(result.current).toEqual(
- Object.assign(
- forms_nbNO[defaultLocale],
- global_nbNO[defaultLocale],
- {
- formatMessage: expect.any(Function),
- }
- )
- )
+ const nb = {}
+ extendDeep(nb, forms_nbNO['nb-NO'], global_nbNO['nb-NO'])
+ nb['formatMessage'] = expect.any(Function)
+
+ expect(result.current).toEqual(nb)
})
it('should inherit locale from shared context', () => {
@@ -39,11 +35,11 @@ describe('Form.useTranslation', () => {
),
})
- expect(resultGB.current).toEqual(
- Object.assign(forms_enGB['en-GB'], global_enGB['en-GB'], {
- formatMessage: expect.any(Function),
- })
- )
+ const gb = {}
+ extendDeep(gb, forms_enGB['en-GB'], global_enGB['en-GB'])
+ gb['formatMessage'] = expect.any(Function)
+
+ expect(resultGB.current).toEqual(gb)
const { result: resultNO } = renderHook(() => useTranslation(), {
wrapper: ({ children }) => (
@@ -51,11 +47,11 @@ describe('Form.useTranslation', () => {
),
})
- expect(resultNO.current).toEqual(
- Object.assign(forms_nbNO['nb-NO'], {
- formatMessage: expect.any(Function),
- })
- )
+ const nb = {}
+ extendDeep(nb, forms_nbNO['nb-NO'], global_nbNO['nb-NO'])
+ nb['formatMessage'] = expect.any(Function)
+
+ expect(resultNO.current).toEqual(nb)
})
it('should extend translation', () => {
diff --git a/packages/dnb-eufemia/src/extensions/forms/hooks/useFieldProps.ts b/packages/dnb-eufemia/src/extensions/forms/hooks/useFieldProps.ts
index 1b6399defc2..03f3bbcdf48 100644
--- a/packages/dnb-eufemia/src/extensions/forms/hooks/useFieldProps.ts
+++ b/packages/dnb-eufemia/src/extensions/forms/hooks/useFieldProps.ts
@@ -75,7 +75,8 @@ export default function useFieldProps<
Value = unknown,
Props extends FieldProps = FieldProps,
>(
- localeProps: Props
+ localeProps: Props,
+ { executeOnChangeRegardlessOfError = false } = {}
): Props & FieldProps & ReturnAdditional {
const { extend } = useContext(FieldPropsContext)
const props = extend(localeProps)
@@ -151,6 +152,7 @@ export default function useFieldProps<
setFieldProps: setPropsDataContext,
setHasVisibleError: setHasVisibleErrorDataContext,
errors: dataContextErrors,
+ showAllErrors,
contextErrorMessages,
} = dataContext ?? {}
const onChangeContext = dataContext?.props?.onChange
@@ -853,7 +855,7 @@ export default function useFieldProps<
)
const callOnChangeContext = useCallback(async () => {
- if (asyncBehaviorIsEnabled) {
+ if (asyncBehaviorIsEnabled && !executeOnChangeRegardlessOfError) {
await yieldAsyncProcess({
name: 'onChangeContext',
waitFor: [
@@ -876,7 +878,7 @@ export default function useFieldProps<
defineAsyncProcess('onChangeContext')
// Skip sync errors, such as required
- if (!hasError()) {
+ if (!hasError() || executeOnChangeRegardlessOfError) {
setEventResult(
(await handlePathChangeDataContext?.(
identifier
@@ -897,14 +899,15 @@ export default function useFieldProps<
forceUpdate()
}, [
asyncBehaviorIsEnabled,
+ executeOnChangeRegardlessOfError,
hasPath,
yieldAsyncProcess,
- identifier,
onChangeContext,
defineAsyncProcess,
hasError,
setEventResult,
handlePathChangeDataContext,
+ identifier,
])
const updateValue = useCallback(
@@ -1166,7 +1169,7 @@ export default function useFieldProps<
])
useEffect(() => {
- if (dataContext.showAllErrors) {
+ if (showAllErrors) {
// In case of async validation, we don't want to show existing errors before the validation has been completed
if (fieldStateRef.current !== 'validating') {
// If showError on a surrounding data context was changed and set to true, it is because the user clicked next, submit or
@@ -1175,7 +1178,7 @@ export default function useFieldProps<
forceUpdate()
}
}
- }, [dataContext.showAllErrors, showError])
+ }, [showAllErrors, showError])
useEffect(() => {
if (
@@ -1312,7 +1315,7 @@ export default function useFieldProps<
}
}
- const fieldSectionProps = {
+ const fieldBlockProps = {
/** Documented APIs */
info: !inFieldBlock ? infoRef.current : undefined,
warning: !inFieldBlock ? warningRef.current : undefined,
@@ -1330,12 +1333,12 @@ export default function useFieldProps<
fieldState: resolveValidatingState(fieldStateRef.current),
}
- const sharedData = useSharedState(id)
- sharedData.set(fieldSectionProps)
+ const sharedData = useSharedState('field-block-props-' + id)
+ sharedData.set(fieldBlockProps)
return {
...props,
- ...fieldSectionProps,
+ ...fieldBlockProps,
/** HTML Attributes */
name: props.name || props.path?.replace('/', '') || id,
@@ -1350,6 +1353,7 @@ export default function useFieldProps<
),
hasError: hasVisibleError,
isChanged: changedRef.current,
+ props,
htmlAttributes,
setHasFocus,
handleFocus,
diff --git a/packages/dnb-eufemia/src/extensions/forms/types.ts b/packages/dnb-eufemia/src/extensions/forms/types.ts
index 08f9ca0a8c1..7b2af337202 100644
--- a/packages/dnb-eufemia/src/extensions/forms/types.ts
+++ b/packages/dnb-eufemia/src/extensions/forms/types.ts
@@ -187,7 +187,7 @@ export type DataValueReadWriteComponentProps<
DataValueReadProps &
DataValueWriteProps
-export type FieldSectionProps = {
+export type FieldBlockProps = {
/**
* The layout of the field block
*/
@@ -240,6 +240,12 @@ export interface UseFieldProps<
*/
htmlAttributes?: Record
+ /**
+ * NB: Undocumented for now.
+ * Forwards all given props in a props object.
+ */
+ props?: Record
+
// - Used by useFieldProps and FieldBlock
info?: React.ReactNode
warning?: React.ReactNode
@@ -341,7 +347,7 @@ export type FieldProps<
Value = unknown,
EmptyValue = undefined | unknown,
ErrorMessages extends DefaultErrorMessages = DefaultErrorMessages,
-> = UseFieldProps & FieldSectionProps
+> = UseFieldProps & FieldBlockProps
export interface FieldHelpProps {
help?: {
diff --git a/packages/dnb-eufemia/src/shared/Translation.tsx b/packages/dnb-eufemia/src/shared/Translation.tsx
index 2fb52563eb2..b6251427c0d 100644
--- a/packages/dnb-eufemia/src/shared/Translation.tsx
+++ b/packages/dnb-eufemia/src/shared/Translation.tsx
@@ -1,4 +1,4 @@
-import { useContext } from 'react'
+import React, { useContext } from 'react'
import {
TranslationArguments,
TranslationId,
@@ -24,8 +24,8 @@ export default function Translation({
const result = formatMessage(id || children, params, translation)
if (typeof result !== 'string') {
- return String(id)
+ return <>{String(id)}>
}
- return result
+ return <>{result}>
}