- {getIcon()}
+ {getFileIcon(file, { isLoading }, hasWarning)}
{getTitle()}
@@ -142,34 +141,6 @@ const UploadFileListCell = ({
)
- function getIcon() {
- if (isLoading) {
- return
- }
-
- if (hasWarning) return
-
- let iconFileType = fileType
-
- if (!iconFileType) {
- const mimeParts = file.type.split('/')
- iconFileType =
- fileExtensionImages[mimeParts[0]] ||
- fileExtensionImages[mimeParts[1]]
- }
-
- if (
- !Object.prototype.hasOwnProperty.call(
- fileExtensionImages,
- iconFileType
- )
- ) {
- iconFileType = 'file'
- }
-
- return
- }
-
function getTitle() {
return isLoading ? (
+ }
+
+ if (hasWarning) return
+
+ let iconFileType = getFileTypeFromExtension(file)
+
+ if (!iconFileType) {
+ const mimeParts = file.type.split('/')
+ iconFileType =
+ fileExtensionImages[mimeParts[0]] ||
+ fileExtensionImages[mimeParts[1]]
+ }
+
+ if (
+ !Object.prototype.hasOwnProperty.call(
+ fileExtensionImages,
+ iconFileType
+ )
+ ) {
+ iconFileType = 'file'
+ }
+
+ return
+}
diff --git a/packages/dnb-eufemia/src/components/upload/UploadFileListLink.tsx b/packages/dnb-eufemia/src/components/upload/UploadFileListLink.tsx
index 9a96178d4ca..6f95d5083ee 100644
--- a/packages/dnb-eufemia/src/components/upload/UploadFileListLink.tsx
+++ b/packages/dnb-eufemia/src/components/upload/UploadFileListLink.tsx
@@ -1,18 +1,25 @@
import React from 'react'
-
+import classNames from 'classnames'
import Anchor from '../../components/Anchor'
import Button from '../button/Button'
+import Span from '../../elements/Span'
import { SpacingProps } from '../space/types'
import { createSpacingClasses } from '../space/SpacingUtils'
-import classNames from 'classnames'
export type UploadFileLinkProps = UploadFileAnchorProps &
UploadFileButtonProps
export const UploadFileLink = (props: UploadFileLinkProps) => {
const { onClick, text, href, download, ...rest } = props
- if (onClick)
+
+ if (!onClick && !href) {
+ return
{text}
+ }
+
+ if (onClick) {
return
+ }
+
return (
{
const spacingClasses = createSpacingClasses(props)
return (
'url')
const defaultProps: UploadAllProps = {
- id: 'id',
acceptedFileTypes: ['png'],
}
@@ -932,6 +931,68 @@ describe('Upload', () => {
screen.queryByText(`file length is more than 5`)
).toBeInTheDocument()
})
+
+ it('keeps files in shared state when providing id', async () => {
+ const files = [
+ createMockFile('fileName1.png', 100, 'image/png'),
+ createMockFile('fileName2.png', 200, 'image/png'),
+ ]
+
+ const id = 'random-id-unmount'
+
+ const { unmount } = render( )
+
+ const inputElement = document.querySelector(
+ '.dnb-upload__file-input'
+ )
+ await waitFor(() =>
+ fireEvent.change(inputElement, {
+ target: { files },
+ })
+ )
+
+ expect(
+ document.querySelectorAll('.dnb-upload__file-cell').length
+ ).toBe(2)
+
+ unmount()
+
+ render( )
+
+ expect(
+ document.querySelectorAll('.dnb-upload__file-cell').length
+ ).toBe(2)
+ })
+
+ it('removes files when unmounting when not providing id', async () => {
+ const files = [
+ createMockFile('fileName1.png', 100, 'image/png'),
+ createMockFile('fileName2.png', 200, 'image/png'),
+ ]
+
+ const { unmount } = render( )
+
+ const inputElement = document.querySelector(
+ '.dnb-upload__file-input'
+ )
+ await waitFor(() =>
+ fireEvent.change(inputElement, {
+ target: { files },
+ })
+ )
+
+ expect(
+ document.querySelectorAll('.dnb-upload__file-cell').length
+ ).toBe(2)
+
+ unmount()
+
+ render( )
+
+ expect(
+ document.querySelectorAll('.dnb-upload__file-cell').length
+ ).toBe(0)
+ })
})
describe('events', () => {
@@ -1017,10 +1078,11 @@ describe('Upload', () => {
'.dnb-upload__file-input'
)
const file1 = createMockFile('fileName-1.png', 100, 'image/png')
+ const file2 = createMockFile('fileName-2.png', 200, 'image/png')
await waitFor(() =>
fireEvent.change(inputElement, {
- target: { files: [file1] },
+ target: { files: [file1, file2] },
})
)
@@ -1031,8 +1093,46 @@ describe('Upload', () => {
await waitFor(() => {
fireEvent.click(fileButton)
expect(
- document.querySelector('.dnb-progress-indicator')
- ).toBeInTheDocument()
+ document.querySelectorAll('.dnb-progress-indicator').length
+ ).toBe(1)
+ })
+ })
+
+ it('will display loading state when async onFileClick fn when files do not have id', async () => {
+ const files = [
+ { file: createMockFile('fileName1.png', 100, 'image/png') },
+ { file: createMockFile('fileName2.png', 100, 'image/png') },
+ { file: createMockFile('fileName3.png', 100, 'image/png') },
+ ]
+
+ const id = 'onFileClick-async-no-id'
+ const onFileClick = jest.fn(async () => {
+ await wait(1)
+ })
+
+ render(
+
+ )
+
+ const MockComponent = () => {
+ const { setFiles } = useUpload(id)
+
+ useEffect(() => setFiles(files), [])
+
+ return
+ }
+
+ render( )
+
+ const fileButton = document.querySelector(
+ '.dnb-upload__file-cell button'
+ )
+
+ await waitFor(() => {
+ fireEvent.click(fileButton)
+ expect(
+ document.querySelectorAll('.dnb-progress-indicator').length
+ ).toBe(1)
})
})
diff --git a/packages/dnb-eufemia/src/components/upload/__tests__/UploadFileListCell.test.tsx b/packages/dnb-eufemia/src/components/upload/__tests__/UploadFileListCell.test.tsx
index 73ac3a5d633..da836a3d61e 100644
--- a/packages/dnb-eufemia/src/components/upload/__tests__/UploadFileListCell.test.tsx
+++ b/packages/dnb-eufemia/src/components/upload/__tests__/UploadFileListCell.test.tsx
@@ -206,6 +206,52 @@ describe('UploadFileListCell', () => {
})
})
+ it('renders a span when file size is 0', () => {
+ const fileName = 'file.png'
+
+ render(
+
+ )
+ expect(screen.queryByText(fileName).tagName).toBe('SPAN')
+ expect(screen.queryByText(fileName)).toHaveClass('dnb-span')
+ })
+
+ it('renders a span when file size is not given', () => {
+ const fileName = 'file.png'
+
+ render(
+
+ )
+ expect(screen.queryByText(fileName).tagName).toBe('SPAN')
+ expect(screen.queryByText(fileName)).toHaveClass('dnb-span')
+ })
+
+ it('renders a button when file size is invalid, but onClick is given', () => {
+ const fileName = 'file.png'
+
+ render(
+
+ )
+
+ expect(screen.queryByText(fileName).parentElement.tagName).toBe(
+ 'BUTTON'
+ )
+ })
+
describe('File Anchor', () => {
it('renders the anchor', () => {
const fileName = 'file.png'
@@ -216,13 +262,14 @@ describe('UploadFileListCell', () => {
uploadFile={{ file: createMockFile(fileName, 100, 'image/png') }}
/>
)
- expect(screen.queryByText(fileName)).toBeInTheDocument()
+ expect(screen.queryByText(fileName).tagName).toBe('A')
})
it('renders the anchor href', () => {
const fileName = 'file.png'
const mockUrl = 'mock-url'
+ const originalCreateObjectURL = global.URL.createObjectURL
global.URL.createObjectURL = jest.fn().mockReturnValueOnce(mockUrl)
render(
@@ -237,6 +284,8 @@ describe('UploadFileListCell', () => {
fileName
) as HTMLAnchorElement
expect(anchorElement.href).toMatch(mockUrl)
+
+ global.URL.createObjectURL = originalCreateObjectURL
})
it('renders the download attribute', () => {
diff --git a/packages/dnb-eufemia/src/components/upload/types.ts b/packages/dnb-eufemia/src/components/upload/types.ts
index 8b3ba2a634b..b51890c958d 100644
--- a/packages/dnb-eufemia/src/components/upload/types.ts
+++ b/packages/dnb-eufemia/src/components/upload/types.ts
@@ -1,6 +1,7 @@
import React from 'react'
import type { SkeletonShow } from '../skeleton/Skeleton'
import type { LocaleProps, SpacingProps } from '../../shared/types'
+import type { SharedStateId } from '../../shared/helpers/useSharedState'
export type UploadAcceptedFileTypes = string[]
@@ -16,7 +17,7 @@ export type UploadProps = {
/**
* unique id used with the useUpload hook to manage the files
*/
- id: string
+ id?: SharedStateId
/**
* list of accepted file types.
diff --git a/packages/dnb-eufemia/src/components/upload/useUpload.ts b/packages/dnb-eufemia/src/components/upload/useUpload.ts
index f0f1e88f8a5..a1f636db152 100644
--- a/packages/dnb-eufemia/src/components/upload/useUpload.ts
+++ b/packages/dnb-eufemia/src/components/upload/useUpload.ts
@@ -1,6 +1,6 @@
import { useCallback, useMemo } from 'react'
import { useSharedState } from '../../shared/helpers/useSharedState'
-import type { UploadFile, UploadFileNative } from './types'
+import type { UploadFile, UploadFileNative, UploadProps } from './types'
export type useUploadReturn = {
files: Array
@@ -16,7 +16,7 @@ export type useUploadReturn = {
/**
* Use together with Upload with the same id to manage the files from outside the component.
*/
-function useUpload(id: string): useUploadReturn {
+function useUpload(id: UploadProps['id']): useUploadReturn {
const { data, extend } = useSharedState<{
files?: Array
internalFiles?: Array
diff --git a/packages/dnb-eufemia/src/extensions/forms/Field/SelectCountry/SelectCountry.tsx b/packages/dnb-eufemia/src/extensions/forms/Field/SelectCountry/SelectCountry.tsx
index 3386daa24c1..c3ffd5f8193 100644
--- a/packages/dnb-eufemia/src/extensions/forms/Field/SelectCountry/SelectCountry.tsx
+++ b/packages/dnb-eufemia/src/extensions/forms/Field/SelectCountry/SelectCountry.tsx
@@ -23,6 +23,7 @@ export type CountryFilterSet =
| 'Nordic'
| 'Europe'
| 'Prioritized'
+export type { CountryType }
export type Props = FieldPropsWithExtraValue<
string,
diff --git a/packages/dnb-eufemia/src/extensions/forms/Field/SelectCountry/__tests__/SelectCountry.test.tsx b/packages/dnb-eufemia/src/extensions/forms/Field/SelectCountry/__tests__/SelectCountry.test.tsx
index 13918582ed5..525fe1db2c6 100644
--- a/packages/dnb-eufemia/src/extensions/forms/Field/SelectCountry/__tests__/SelectCountry.test.tsx
+++ b/packages/dnb-eufemia/src/extensions/forms/Field/SelectCountry/__tests__/SelectCountry.test.tsx
@@ -391,11 +391,11 @@ describe('Field.SelectCountry', () => {
return `${country.name} (${value})`
}
})
- const transformIn = jest.fn((value) => {
- return String(value).match(/\((.*)\)/)?.[1]
+ const transformIn = jest.fn((external) => {
+ return String(external).match(/\((.*)\)/)?.[1] || external
})
- const valueTransformIn = jest.fn((value) => {
- return String(value).match(/\((.*)\)/)?.[1]
+ const valueTransformIn = jest.fn((internal) => {
+ return String(internal).match(/\((.*)\)/)?.[1]
})
const onSubmit = jest.fn()
@@ -434,7 +434,7 @@ describe('Field.SelectCountry', () => {
}
expect(transformOut).toHaveBeenCalledTimes(1)
- expect(transformIn).toHaveBeenCalledTimes(4)
+ expect(transformIn).toHaveBeenCalledTimes(3)
expect(valueTransformIn).toHaveBeenCalledTimes(2)
const firstItemElement = () =>
@@ -452,7 +452,7 @@ describe('Field.SelectCountry', () => {
)
expect(transformOut).toHaveBeenCalledTimes(1)
- expect(transformIn).toHaveBeenCalledTimes(5)
+ expect(transformIn).toHaveBeenCalledTimes(4)
expect(valueTransformIn).toHaveBeenCalledTimes(3)
expect(input).toHaveValue('Norge')
@@ -468,7 +468,7 @@ describe('Field.SelectCountry', () => {
expect(value).toHaveTextContent('Sveits')
expect(transformOut).toHaveBeenCalledTimes(4)
- expect(transformIn).toHaveBeenCalledTimes(8)
+ expect(transformIn).toHaveBeenCalledTimes(6)
expect(valueTransformIn).toHaveBeenCalledTimes(4)
fireEvent.submit(form)
@@ -479,7 +479,7 @@ describe('Field.SelectCountry', () => {
)
expect(transformOut).toHaveBeenCalledTimes(4)
- expect(transformIn).toHaveBeenCalledTimes(9)
+ expect(transformIn).toHaveBeenCalledTimes(7)
expect(valueTransformIn).toHaveBeenCalledTimes(5)
expect(transformOut).toHaveBeenNthCalledWith(1, 'NO', NO)
@@ -487,15 +487,13 @@ describe('Field.SelectCountry', () => {
expect(transformOut).toHaveBeenNthCalledWith(3, 'CH', CH)
expect(transformOut).toHaveBeenNthCalledWith(4, 'CH', CH)
- expect(transformIn).toHaveBeenNthCalledWith(1, undefined)
- expect(transformIn).toHaveBeenNthCalledWith(2, undefined)
+ expect(transformIn).toHaveBeenNthCalledWith(1, 'NO')
+ expect(transformIn).toHaveBeenNthCalledWith(2, 'NO')
expect(transformIn).toHaveBeenNthCalledWith(3, 'Norge (NO)')
expect(transformIn).toHaveBeenNthCalledWith(4, 'Norge (NO)')
expect(transformIn).toHaveBeenNthCalledWith(5, 'Norge (NO)')
- expect(transformIn).toHaveBeenNthCalledWith(6, 'Norge (NO)')
+ expect(transformIn).toHaveBeenNthCalledWith(6, 'Sveits (CH)')
expect(transformIn).toHaveBeenNthCalledWith(7, 'Sveits (CH)')
- expect(transformIn).toHaveBeenNthCalledWith(8, 'Sveits (CH)')
- expect(transformIn).toHaveBeenNthCalledWith(9, 'Sveits (CH)')
expect(valueTransformIn).toHaveBeenNthCalledWith(1, undefined)
expect(valueTransformIn).toHaveBeenNthCalledWith(2, 'Norge (NO)')
@@ -504,6 +502,121 @@ describe('Field.SelectCountry', () => {
expect(valueTransformIn).toHaveBeenNthCalledWith(5, 'Sveits (CH)')
})
+ it('should support "transformIn" and "transformOut" when value is given by the data context', async () => {
+ const transformOut = jest.fn((value, country) => {
+ if (value) {
+ return `${country.name} (${value})`
+ }
+ })
+ const transformIn = jest.fn((external) => {
+ return String(external).match(/\((.*)\)/)?.[1]
+ })
+ const valueTransformIn = jest.fn((internal) => {
+ return String(internal).match(/\((.*)\)/)?.[1]
+ })
+
+ const onSubmit = jest.fn()
+
+ render(
+
+
+
+
+
+ )
+
+ const NO = {
+ cdc: '47',
+ continent: 'Europe',
+ i18n: { en: 'Norway', nb: 'Norge' },
+ iso: 'NO',
+ name: 'Norge',
+ regions: ['Scandinavia', 'Nordic'],
+ }
+
+ const CH = {
+ cdc: '41',
+ continent: 'Europe',
+ i18n: { en: 'Switzerland', nb: 'Sveits' },
+ iso: 'CH',
+ name: 'Sveits',
+ }
+
+ expect(transformOut).toHaveBeenCalledTimes(0)
+ expect(transformIn).toHaveBeenCalledTimes(1)
+ expect(valueTransformIn).toHaveBeenCalledTimes(1)
+
+ const firstItemElement = () =>
+ document.querySelectorAll('li.dnb-drawer-list__option')[0]
+
+ const form = document.querySelector('form')
+ const input = document.querySelector('input')
+ const value = document.querySelector('.dnb-forms-value-block__content')
+
+ fireEvent.submit(form)
+ expect(onSubmit).toHaveBeenCalledTimes(1)
+ expect(onSubmit).toHaveBeenLastCalledWith(
+ { country: 'Norge (NO)' },
+ expect.anything()
+ )
+
+ expect(transformOut).toHaveBeenCalledTimes(0)
+ expect(transformIn).toHaveBeenCalledTimes(2)
+ expect(valueTransformIn).toHaveBeenCalledTimes(2)
+
+ expect(input).toHaveValue('Norge')
+ expect(value).toHaveTextContent('Norge')
+
+ await userEvent.type(input, '{Backspace>10}Sveits')
+ await waitFor(() => {
+ expect(firstItemElement()).toBeInTheDocument()
+ })
+ await userEvent.click(firstItemElement())
+
+ expect(input).toHaveValue('Sveits')
+ expect(value).toHaveTextContent('Sveits')
+
+ expect(transformOut).toHaveBeenCalledTimes(3)
+ expect(transformIn).toHaveBeenCalledTimes(4)
+ expect(valueTransformIn).toHaveBeenCalledTimes(3)
+
+ fireEvent.submit(form)
+ expect(onSubmit).toHaveBeenCalledTimes(2)
+ expect(onSubmit).toHaveBeenLastCalledWith(
+ { country: 'Sveits (CH)' },
+ expect.anything()
+ )
+
+ expect(transformOut).toHaveBeenCalledTimes(3)
+ expect(transformIn).toHaveBeenCalledTimes(5)
+ expect(valueTransformIn).toHaveBeenCalledTimes(4)
+
+ expect(transformOut).toHaveBeenNthCalledWith(1, 'NO', NO)
+ expect(transformOut).toHaveBeenNthCalledWith(2, 'CH', CH)
+ expect(transformOut).toHaveBeenNthCalledWith(3, 'CH', CH)
+
+ expect(transformIn).toHaveBeenNthCalledWith(1, 'Norge (NO)')
+ expect(transformIn).toHaveBeenNthCalledWith(2, 'Norge (NO)')
+ expect(transformIn).toHaveBeenNthCalledWith(3, 'Norge (NO)')
+ expect(transformIn).toHaveBeenNthCalledWith(4, 'Sveits (CH)')
+ expect(transformIn).toHaveBeenNthCalledWith(5, 'Sveits (CH)')
+
+ expect(valueTransformIn).toHaveBeenNthCalledWith(1, 'Norge (NO)')
+ expect(valueTransformIn).toHaveBeenNthCalledWith(2, 'Norge (NO)')
+ expect(valueTransformIn).toHaveBeenNthCalledWith(3, 'Sveits (CH)')
+ expect(valueTransformIn).toHaveBeenNthCalledWith(4, 'Sveits (CH)')
+ })
+
it('should store "displayValue" in data context', async () => {
let dataContext = null
diff --git a/packages/dnb-eufemia/src/extensions/forms/Field/SelectCountry/stories/SelectCountry.stories.tsx b/packages/dnb-eufemia/src/extensions/forms/Field/SelectCountry/stories/SelectCountry.stories.tsx
index b32f0ff6bf2..86315700e30 100644
--- a/packages/dnb-eufemia/src/extensions/forms/Field/SelectCountry/stories/SelectCountry.stories.tsx
+++ b/packages/dnb-eufemia/src/extensions/forms/Field/SelectCountry/stories/SelectCountry.stories.tsx
@@ -24,13 +24,13 @@ export function SelectCountry() {
)
}
-const transformOut = (value, country: CountryType) => {
- if (value) {
- return `${country.name} (${value})`
+const transformOut = (internal: string, country: CountryType) => {
+ if (internal) {
+ return `${country.name} (${internal})`
}
}
-const transformIn = (value) => {
- return String(value).match(/\((.*)\)/)?.[1]
+const transformIn = (external: unknown) => {
+ return String(external).match(/\((.*)\)/)?.[1] || 'NO'
}
export function Transform() {
diff --git a/packages/dnb-eufemia/src/extensions/forms/Field/Slider/Slider.tsx b/packages/dnb-eufemia/src/extensions/forms/Field/Slider/Slider.tsx
index 1d39e17602c..9c25cd6b6bd 100644
--- a/packages/dnb-eufemia/src/extensions/forms/Field/Slider/Slider.tsx
+++ b/packages/dnb-eufemia/src/extensions/forms/Field/Slider/Slider.tsx
@@ -56,7 +56,9 @@ function SliderComponent(props: Props) {
[getSourceValue]
)
- const value = getValues(props.paths ?? props.path ?? props.value)
+ const value = getValues(
+ props.paths ?? props.path ?? props.value ?? props.defaultValue
+ )
const preparedProps = {
...props,
step: getSourceValue(props.step),
diff --git a/packages/dnb-eufemia/src/extensions/forms/Field/Slider/SliderDocs.ts b/packages/dnb-eufemia/src/extensions/forms/Field/Slider/SliderDocs.ts
index 6e341801b0d..66ad71ab130 100644
--- a/packages/dnb-eufemia/src/extensions/forms/Field/Slider/SliderDocs.ts
+++ b/packages/dnb-eufemia/src/extensions/forms/Field/Slider/SliderDocs.ts
@@ -5,7 +5,7 @@ export const SliderFieldProperties: PropertiesTableProps = {
paths: {
doc: 'Define an array with JSON Pointer paths for multiple thumb buttons.',
type: 'Array',
- status: 'required',
+ status: 'optional',
},
min: SliderProperties.min,
max: SliderProperties.max,
diff --git a/packages/dnb-eufemia/src/extensions/forms/Field/Slider/__tests__/Slider.test.tsx b/packages/dnb-eufemia/src/extensions/forms/Field/Slider/__tests__/Slider.test.tsx
index ddb8218ce96..89d08e455c5 100644
--- a/packages/dnb-eufemia/src/extensions/forms/Field/Slider/__tests__/Slider.test.tsx
+++ b/packages/dnb-eufemia/src/extensions/forms/Field/Slider/__tests__/Slider.test.tsx
@@ -35,6 +35,20 @@ describe('Field.Slider', () => {
expect(parseFloat(getButtonHelper().value)).toBe(value + 10)
})
+ it('with "defaultValue"', () => {
+ const value = 70
+
+ render(
+
+
+
+ )
+
+ fireEvent.submit(document.querySelector('form'))
+
+ expect(parseFloat(getButtonHelper().value)).toBe(value)
+ })
+
it('with "path"', () => {
render(
diff --git a/packages/dnb-eufemia/src/extensions/forms/Field/String/StringDocs.ts b/packages/dnb-eufemia/src/extensions/forms/Field/String/StringDocs.ts
index ffcc2c0f28f..247e0634fd8 100644
--- a/packages/dnb-eufemia/src/extensions/forms/Field/String/StringDocs.ts
+++ b/packages/dnb-eufemia/src/extensions/forms/Field/String/StringDocs.ts
@@ -53,7 +53,7 @@ export const stringProperties: PropertiesTableProps = {
status: 'optional',
},
width: {
- doc: '`false` for no width (use browser default), small, medium or large for predefined standard widths, stretch for fill available width.',
+ doc: '`false` for no width (use browser default), small, medium or large for predefined standard widths, stretch to fill available width.',
type: ['string', 'false'],
status: 'optional',
},
diff --git a/packages/dnb-eufemia/src/extensions/forms/Field/String/__tests__/String.test.tsx b/packages/dnb-eufemia/src/extensions/forms/Field/String/__tests__/String.test.tsx
index 2e36e267f57..509a660a2b5 100644
--- a/packages/dnb-eufemia/src/extensions/forms/Field/String/__tests__/String.test.tsx
+++ b/packages/dnb-eufemia/src/extensions/forms/Field/String/__tests__/String.test.tsx
@@ -263,7 +263,7 @@ describe('Field.String', () => {
const input = document.querySelector('input')
expect(input).toHaveValue('XYZ')
- expect(transformIn).toHaveBeenCalledTimes(2)
+ expect(transformIn).toHaveBeenCalledTimes(1)
expect(transformIn).toHaveBeenLastCalledWith('xYz')
expect(transformOut).toHaveBeenCalledTimes(0)
expect(onChangeProvider).toHaveBeenCalledTimes(0)
@@ -272,7 +272,7 @@ describe('Field.String', () => {
await userEvent.type(input, '{Backspace>3}aBc')
expect(input).toHaveValue('ABC')
- expect(transformIn).toHaveBeenCalledTimes(16)
+ expect(transformIn).toHaveBeenCalledTimes(9)
expect(transformIn).toHaveBeenLastCalledWith('abc')
expect(transformOut).toHaveBeenCalledTimes(13)
expect(transformOut).toHaveBeenLastCalledWith('ABc', undefined)
@@ -287,7 +287,7 @@ describe('Field.String', () => {
await userEvent.type(input, '{Backspace>3}EfG')
expect(input).toHaveValue('EFG')
- expect(transformIn).toHaveBeenCalledTimes(29)
+ expect(transformIn).toHaveBeenCalledTimes(16)
expect(transformIn).toHaveBeenLastCalledWith('efg')
expect(transformOut).toHaveBeenCalledTimes(25)
expect(transformOut).toHaveBeenLastCalledWith('EFG', undefined)
@@ -305,6 +305,9 @@ describe('Field.String', () => {
return { value, foo: 'bar' }
})
const transformIn = jest.fn((data) => {
+ if (typeof data === 'string') {
+ return data
+ }
return data?.value
})
const valueTransformIn = jest.fn((data) => {
@@ -327,12 +330,14 @@ describe('Field.String', () => {
)
expect(transformOut).toHaveBeenCalledTimes(1)
- expect(transformIn).toHaveBeenCalledTimes(4)
+ expect(transformIn).toHaveBeenCalledTimes(3)
expect(valueTransformIn).toHaveBeenCalledTimes(2)
const form = document.querySelector('form')
const input = document.querySelector('input')
+ expect(input).toHaveValue('A')
+
fireEvent.submit(form)
expect(onSubmit).toHaveBeenCalledTimes(1)
expect(onSubmit).toHaveBeenLastCalledWith(
@@ -346,7 +351,7 @@ describe('Field.String', () => {
)
expect(transformOut).toHaveBeenCalledTimes(1)
- expect(transformIn).toHaveBeenCalledTimes(5)
+ expect(transformIn).toHaveBeenCalledTimes(4)
expect(valueTransformIn).toHaveBeenCalledTimes(3)
expect(input).toHaveValue('A')
@@ -362,7 +367,7 @@ describe('Field.String', () => {
).toHaveTextContent('B')
expect(transformOut).toHaveBeenCalledTimes(6)
- expect(transformIn).toHaveBeenCalledTimes(9)
+ expect(transformIn).toHaveBeenCalledTimes(6)
expect(valueTransformIn).toHaveBeenCalledTimes(5)
fireEvent.submit(form)
@@ -378,7 +383,7 @@ describe('Field.String', () => {
)
expect(transformOut).toHaveBeenCalledTimes(6)
- expect(transformIn).toHaveBeenCalledTimes(10)
+ expect(transformIn).toHaveBeenCalledTimes(7)
expect(valueTransformIn).toHaveBeenCalledTimes(6)
expect(transformOut).toHaveBeenNthCalledWith(1, 'A', undefined)
@@ -396,8 +401,8 @@ describe('Field.String', () => {
)
expect(transformOut).toHaveBeenNthCalledWith(6, 'B', undefined)
- expect(transformIn).toHaveBeenNthCalledWith(1, undefined)
- expect(transformIn).toHaveBeenNthCalledWith(2, undefined)
+ expect(transformIn).toHaveBeenNthCalledWith(1, 'A')
+ expect(transformIn).toHaveBeenNthCalledWith(2, 'A')
expect(transformIn).toHaveBeenNthCalledWith(3, {
foo: 'bar',
value: 'A',
@@ -407,26 +412,14 @@ describe('Field.String', () => {
value: 'A',
})
expect(transformIn).toHaveBeenNthCalledWith(5, {
- foo: 'bar',
- value: 'A',
- })
- expect(transformIn).toHaveBeenNthCalledWith(6, {
- foo: 'bar',
- value: undefined,
- })
- expect(transformIn).toHaveBeenNthCalledWith(7, {
foo: 'bar',
value: undefined,
})
- expect(transformIn).toHaveBeenNthCalledWith(8, {
- foo: 'bar',
- value: 'B',
- })
- expect(transformIn).toHaveBeenNthCalledWith(9, {
+ expect(transformIn).toHaveBeenNthCalledWith(6, {
foo: 'bar',
value: 'B',
})
- expect(transformIn).toHaveBeenNthCalledWith(10, {
+ expect(transformIn).toHaveBeenNthCalledWith(7, {
foo: 'bar',
value: 'B',
})
diff --git a/packages/dnb-eufemia/src/extensions/forms/Field/String/stories/String.stories.tsx b/packages/dnb-eufemia/src/extensions/forms/Field/String/stories/String.stories.tsx
index 0e132a3a0da..96197cf175e 100644
--- a/packages/dnb-eufemia/src/extensions/forms/Field/String/stories/String.stories.tsx
+++ b/packages/dnb-eufemia/src/extensions/forms/Field/String/stories/String.stories.tsx
@@ -5,8 +5,33 @@ import { Flex } from '../../../../../components'
export default {
title: 'Eufemia/Extensions/Forms/String',
}
+export const StringAndLabelStretch = () => {
+ return (
+
+ Subheading
+
+
+
+ )
+}
-export const String = () => {
+export const StringExample = () => {
return (
@@ -35,11 +60,11 @@ export const String = () => {
}
export const Transform = () => {
- const transformIn = (value) => {
- return value?.toUpperCase()
+ const transformIn = (external: unknown) => {
+ return String(external)?.toUpperCase()
}
- const transformOut = (value) => {
- return value?.toLowerCase()
+ const transformOut = (internal: string) => {
+ return internal?.toLowerCase()
}
return (
diff --git a/packages/dnb-eufemia/src/extensions/forms/Field/Upload/Upload.tsx b/packages/dnb-eufemia/src/extensions/forms/Field/Upload/Upload.tsx
index 32796efcf1d..b36bdcc99db 100644
--- a/packages/dnb-eufemia/src/extensions/forms/Field/Upload/Upload.tsx
+++ b/packages/dnb-eufemia/src/extensions/forms/Field/Upload/Upload.tsx
@@ -23,6 +23,7 @@ import { useTranslation as useSharedTranslation } from '../../../../shared'
import { SpacingProps } from '../../../../shared/types'
import { FormError } from '../../utils'
+export type { UploadFile, UploadFileNative }
export type UploadValue = Array
export type Props = Omit<
FieldProps,
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
index 1dc6d31013a..c4a14a91047 100644
--- 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
@@ -1,4 +1,4 @@
-import React from 'react'
+import React, { useContext } from 'react'
import { fireEvent, render, waitFor, screen } from '@testing-library/react'
import { DataContext, Field, Form, Wizard } from '../../..'
import { BYTES_IN_A_MEGA_BYTE } from '../../../../../components/upload/UploadVerify'
@@ -7,7 +7,7 @@ import { createMockFile } from '../../../../../components/upload/__tests__/testH
import nbNOForms from '../../../constants/locales/nb-NO'
import nbNOShared from '../../../../../shared/locales/nb-NO'
import userEvent from '@testing-library/user-event'
-import { UploadValue } from '../Upload'
+import { UploadFileNative, UploadValue } from '../Upload'
import { wait } from '../../../../../core/jest/jestSetup'
const nbForms = nbNOForms['nb-NO']
@@ -1527,4 +1527,155 @@ describe('Field.Upload', () => {
document.querySelectorAll('.dnb-upload__file-cell').length
).toBe(0)
})
+
+ describe('transformIn and transformOut', () => {
+ type DocumentMetadata = {
+ id: string
+ fileName: string
+ }
+
+ const defaultValue = [
+ {
+ id: '1234',
+ fileName: 'myFile.pdf',
+ },
+ ] satisfies DocumentMetadata[] as unknown as UploadValue
+
+ const filesCache = new Map()
+
+ // To the Field (from e.g. defaultValue)
+ const transformIn = (external?: DocumentMetadata[]) => {
+ return (
+ external?.map(({ id, fileName }) => {
+ const file: File = filesCache.get(id) || new File([], fileName)
+
+ return { id, file } satisfies UploadFileNative
+ }) || []
+ )
+ }
+
+ // From the Field (internal value) to the data context or event parameter
+ const transformOut = (internal?: UploadValue) => {
+ return (
+ internal?.map(({ id, file }) => {
+ if (!filesCache.has(id)) {
+ filesCache.set(id, file)
+ }
+
+ return { id, fileName: file.name } satisfies DocumentMetadata
+ }) || []
+ )
+ }
+
+ let dataContext = null
+ function LogContext() {
+ dataContext = useContext(DataContext.Context).data
+ return null
+ }
+
+ it('should render files given in data context', async () => {
+ render(
+
+
+
+
+ )
+
+ expect(
+ document.querySelectorAll('.dnb-upload__file-cell').length
+ ).toBe(1)
+ expect(dataContext).toEqual({
+ documents: [
+ {
+ id: '1234',
+ fileName: 'myFile.pdf',
+ },
+ ],
+ })
+
+ const file = createMockFile('secondFile.png', 100, 'image/png')
+ await waitFor(() => {
+ fireEvent.drop(document.querySelector('input'), {
+ dataTransfer: {
+ files: [file],
+ },
+ })
+ })
+
+ expect(
+ document.querySelectorAll('.dnb-upload__file-cell').length
+ ).toBe(2)
+ expect(dataContext).toEqual({
+ documents: [
+ {
+ id: '1234',
+ fileName: 'myFile.pdf',
+ },
+ {
+ id: expect.any(String),
+ fileName: 'secondFile.png',
+ },
+ ],
+ })
+ })
+
+ it('should render files given by defaultValue', async () => {
+ render(
+
+
+
+
+ )
+
+ expect(
+ document.querySelectorAll('.dnb-upload__file-cell').length
+ ).toBe(1)
+ expect(dataContext).toEqual({
+ documents: [
+ {
+ id: '1234',
+ fileName: 'myFile.pdf',
+ },
+ ],
+ })
+
+ const file = createMockFile('secondFile.png', 100, 'image/png')
+ await waitFor(() => {
+ fireEvent.drop(document.querySelector('input'), {
+ dataTransfer: {
+ files: [file],
+ },
+ })
+ })
+
+ expect(
+ document.querySelectorAll('.dnb-upload__file-cell').length
+ ).toBe(2)
+ expect(dataContext).toEqual({
+ documents: [
+ {
+ id: '1234',
+ fileName: 'myFile.pdf',
+ },
+ {
+ id: expect.any(String),
+ fileName: 'secondFile.png',
+ },
+ ],
+ })
+ })
+ })
})
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
index db2837a8397..131fd1afffb 100644
--- 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
@@ -1,5 +1,6 @@
import { Field, Form, Tools } from '../../..'
import { Flex } from '../../../../../components'
+import { UploadFileNative } from '../../../../../components/Upload'
import { createRequest } from '../../../Form/Handler/stories/FormHandler.stories'
import { UploadValue } from '../Upload'
@@ -167,3 +168,68 @@ export const AsyncEverything = () => {
)
}
+
+interface DocumentMetadata {
+ id: string
+ fileName: string
+}
+
+const defaultValue = [
+ {
+ id: '1234',
+ fileName: 'myFile.pdf',
+ },
+] satisfies DocumentMetadata[] as unknown as UploadValue
+
+const filesCache = new Map()
+
+// To the Field (from e.g. defaultValue)
+const transformIn = (external?: DocumentMetadata[]) => {
+ return (
+ external?.map(({ id, fileName }) => {
+ const file: File = filesCache.get(id) || new File([], fileName)
+
+ return { id, file } satisfies UploadFileNative
+ }) || []
+ )
+}
+
+// From the Field (internal value) to the data context or event parameter
+const transformOut = (internal?: UploadValue) => {
+ return (
+ internal?.map(({ id, file }) => {
+ if (!filesCache.has(id)) {
+ filesCache.set(id, file)
+ }
+
+ return { id, fileName: file.name } satisfies DocumentMetadata
+ }) || []
+ )
+}
+
+export function TransformInAndOut() {
+ return (
+
+
+ {
+ console.log('onFileClick', fileItem)
+ }}
+ />
+
+
+
+
+
+ )
+}
diff --git a/packages/dnb-eufemia/src/extensions/forms/FieldBlock/__tests__/__image_snapshots__/fieldblock-for-sbanken-have-to-match-widths.snap.png b/packages/dnb-eufemia/src/extensions/forms/FieldBlock/__tests__/__image_snapshots__/fieldblock-for-sbanken-have-to-match-widths.snap.png
index 5046786f8b3..a76cf637d3d 100644
Binary files a/packages/dnb-eufemia/src/extensions/forms/FieldBlock/__tests__/__image_snapshots__/fieldblock-for-sbanken-have-to-match-widths.snap.png and b/packages/dnb-eufemia/src/extensions/forms/FieldBlock/__tests__/__image_snapshots__/fieldblock-for-sbanken-have-to-match-widths.snap.png differ
diff --git a/packages/dnb-eufemia/src/extensions/forms/FieldBlock/__tests__/__image_snapshots__/fieldblock-for-ui-have-to-match-widths.snap.png b/packages/dnb-eufemia/src/extensions/forms/FieldBlock/__tests__/__image_snapshots__/fieldblock-for-ui-have-to-match-widths.snap.png
index 0cad8a8600d..a6ab2aa89ea 100644
Binary files a/packages/dnb-eufemia/src/extensions/forms/FieldBlock/__tests__/__image_snapshots__/fieldblock-for-ui-have-to-match-widths.snap.png and b/packages/dnb-eufemia/src/extensions/forms/FieldBlock/__tests__/__image_snapshots__/fieldblock-for-ui-have-to-match-widths.snap.png differ
diff --git a/packages/dnb-eufemia/src/extensions/forms/FieldBlock/style/dnb-field-block.scss b/packages/dnb-eufemia/src/extensions/forms/FieldBlock/style/dnb-field-block.scss
index d66077b4019..105d4878964 100644
--- a/packages/dnb-eufemia/src/extensions/forms/FieldBlock/style/dnb-field-block.scss
+++ b/packages/dnb-eufemia/src/extensions/forms/FieldBlock/style/dnb-field-block.scss
@@ -45,6 +45,9 @@ fieldset.dnb-forms-field-block {
&--width {
&-stretch {
flex-grow: 1;
+ label.dnb-form-label {
+ max-width: none;
+ }
}
@include allAbove(x-small) {
&-custom {
diff --git a/packages/dnb-eufemia/src/extensions/forms/Value/Upload/Upload.tsx b/packages/dnb-eufemia/src/extensions/forms/Value/Upload/Upload.tsx
index ff0cb78f6a7..d12447ce4a5 100644
--- a/packages/dnb-eufemia/src/extensions/forms/Value/Upload/Upload.tsx
+++ b/packages/dnb-eufemia/src/extensions/forms/Value/Upload/Upload.tsx
@@ -1,21 +1,18 @@
-import React, { useMemo } from 'react'
+import React, { useMemo, useState } from 'react'
import classnames from 'classnames'
import { useValueProps } from '../../hooks'
import { ValueProps } from '../../types'
import ValueBlock from '../../ValueBlock'
-import Icon from '../../../../components/Icon'
import ListFormat, {
ListFormatProps,
} from '../../../../components/list-format'
import type { UploadFile } from '../../../../components/upload/types'
-import { fileExtensionImages } from '../../../../components/upload/UploadFileListCell'
-import {
- BYTES_IN_A_MEGA_BYTE,
- getFileTypeFromExtension,
-} from '../../../../components/upload/UploadVerify'
+import { getFileIcon } from '../../../../components/upload/UploadFileListCell'
+import { BYTES_IN_A_MEGA_BYTE } from '../../../../components/upload/UploadVerify'
import { Props as FieldUploadProps } from '../../Field/Upload/Upload'
import { format } from '../../../../components/number-format/NumberUtils'
import { UploadFileLink } from '../../../../components/upload/UploadFileListLink'
+import { isAsync } from '../../../../shared/helpers/isAsync'
export type Props = ValueProps> &
Omit &
@@ -40,33 +37,18 @@ function Upload(props: Props) {
const list = useMemo(() => {
const valueToUse =
value?.map((uploadFile, index) => {
- const { file } = uploadFile || {}
- if (!file) {
+ if (!uploadFile) {
return
}
- const onFileClickHandler = () => {
- if (typeof onFileClick === 'function') {
- onFileClick({ fileItem: uploadFile })
- }
- }
-
- const imageUrl = URL.createObjectURL(file)
-
- const text =
- file.name + (displaySize ? ' ' + getSize(file.size) : '')
return (
-
- {getIcon(file)}
-
-
-
+
)
}) || undefined
@@ -103,32 +85,60 @@ function getSize(size: number) {
})} MB)`
}
-function getIcon(file: File) {
+Upload._supportsSpacingProps = true
+export default Upload
+
+function UploadFileItem(
+ props: { uploadFile: UploadFile } & Pick<
+ Props,
+ 'download' | 'onFileClick' | 'displaySize'
+ >
+) {
+ const {
+ uploadFile,
+ download = false,
+ displaySize = false,
+ onFileClick,
+ } = props
+
+ const [loading, setLoading] = useState(false)
+
+ const { file, isLoading: fileIsLoading } = uploadFile || {}
+
if (!file) {
- return
+ return null
}
- const fileType = getFileTypeFromExtension(file)
- let iconFileType = fileType
-
- if (!iconFileType) {
- const mimeParts = file.type.split('/')
- iconFileType =
- fileExtensionImages[mimeParts[0]] ||
- fileExtensionImages[mimeParts[1]]
+ const handleFileClickAsync = async (uploadFile: UploadFile) => {
+ setLoading(true)
+ await onFileClick({ fileItem: uploadFile })
+ setLoading(false)
}
- if (
- !Object.prototype.hasOwnProperty.call(
- fileExtensionImages,
- iconFileType
- )
- ) {
- iconFileType = 'file'
+ const onFileClickHandler = async () => {
+ if (typeof onFileClick === 'function') {
+ if (isAsync(onFileClick)) {
+ handleFileClickAsync(uploadFile)
+ } else {
+ onFileClick({ fileItem: uploadFile })
+ }
+ }
}
- return
-}
+ const imageUrl = file?.size > 0 ? URL.createObjectURL(file) : null
-Upload._supportsSpacingProps = true
-export default Upload
+ const text = file.name + (displaySize ? ' ' + getSize(file.size) : '')
+ const isLoading = fileIsLoading || loading
+ return (
+
+ {getFileIcon(file, { isLoading, size: 'medium' }, false)}
+
+
+ )
+}
diff --git a/packages/dnb-eufemia/src/extensions/forms/Value/Upload/__tests__/Upload.screenshot.test.ts b/packages/dnb-eufemia/src/extensions/forms/Value/Upload/__tests__/Upload.screenshot.test.ts
index 410e3b7a300..24a01119b78 100644
--- a/packages/dnb-eufemia/src/extensions/forms/Value/Upload/__tests__/Upload.screenshot.test.ts
+++ b/packages/dnb-eufemia/src/extensions/forms/Value/Upload/__tests__/Upload.screenshot.test.ts
@@ -22,17 +22,47 @@ describe('Value.Upload', () => {
expect(screenshot).toMatchImageSnapshot()
})
- it('have to list upload inline', async () => {
+ it('have to match list upload inline', async () => {
const screenshot = await makeScreenshot({
selector: '[data-visual-test="upload-value-inline"]',
})
expect(screenshot).toMatchImageSnapshot()
})
- it('have to list upload value', async () => {
+ it('have to match label and value', async () => {
+ const screenshot = await makeScreenshot({
+ selector: '[data-visual-test="upload-value-label-and-value"]',
+ })
+ expect(screenshot).toMatchImageSnapshot()
+ })
+
+ it('have to match label and value with on file click', async () => {
+ const screenshot = await makeScreenshot({
+ selector:
+ '[data-visual-test="upload-value-label-and-value-on-file-click"]',
+ })
+ expect(screenshot).toMatchImageSnapshot()
+ })
+
+ it('have to match list', async () => {
const screenshot = await makeScreenshot({
selector: '[data-visual-test="upload-value-lists"]',
})
expect(screenshot).toMatchImageSnapshot()
})
+
+ it('have to match list with on file click', async () => {
+ const screenshot = await makeScreenshot({
+ selector: '[data-visual-test="upload-value-lists-on-file-click"]',
+ })
+ expect(screenshot).toMatchImageSnapshot()
+ })
+
+ it('have to match files as non-clickable', async () => {
+ const screenshot = await makeScreenshot({
+ selector:
+ '[data-visual-test="upload-value-display-file-as-non-clickable"]',
+ })
+ expect(screenshot).toMatchImageSnapshot()
+ })
})
diff --git a/packages/dnb-eufemia/src/extensions/forms/Value/Upload/__tests__/Upload.test.tsx b/packages/dnb-eufemia/src/extensions/forms/Value/Upload/__tests__/Upload.test.tsx
index 3840bf114d2..b727da994fb 100644
--- a/packages/dnb-eufemia/src/extensions/forms/Value/Upload/__tests__/Upload.test.tsx
+++ b/packages/dnb-eufemia/src/extensions/forms/Value/Upload/__tests__/Upload.test.tsx
@@ -1,7 +1,8 @@
import React from 'react'
-import { screen, render, fireEvent } from '@testing-library/react'
+import { screen, render, fireEvent, waitFor } from '@testing-library/react'
import { Value, Form } from '../../..'
import { createMockFile } from '../../../../../components/upload/__tests__/testHelpers'
+import { wait } from '../../../../../core/jest/jestSetup'
global.URL.createObjectURL = jest.fn(() => 'url')
@@ -321,6 +322,24 @@ describe('Value.Upload', () => {
})
})
+ it('renders a span when file size is 0', () => {
+ const fileName = 'file.png'
+
+ render(
+
+ )
+ expect(screen.queryByText(fileName).tagName).toBe('SPAN')
+ expect(screen.queryByText(fileName)).toHaveClass('dnb-span')
+ })
+
describe('File Anchor', () => {
it('renders the anchor', () => {
const fileName = 'file.png'
@@ -336,7 +355,7 @@ describe('Value.Upload', () => {
]}
/>
)
- expect(screen.queryByText(fileName)).toBeInTheDocument()
+ expect(screen.queryByText(fileName).tagName).toBe('A')
})
it('executes onFileClick event when button is clicked', () => {
@@ -363,10 +382,58 @@ describe('Value.Upload', () => {
expect(onFileClick).toHaveBeenCalledTimes(1)
})
+ it('should display spinner when async onFileClick event', async () => {
+ const onFileClick = jest.fn(async () => {
+ await wait(1)
+ })
+
+ render(
+
+ )
+
+ const buttonElement = document.querySelector('.dnb-button')
+
+ await waitFor(() => {
+ fireEvent.click(buttonElement)
+ expect(
+ document.querySelector('.dnb-progress-indicator')
+ ).toBeInTheDocument()
+ })
+ })
+
+ it('should display spinner when file is loading', async () => {
+ render(
+
+ )
+
+ expect(
+ document.querySelector('.dnb-progress-indicator')
+ ).toBeInTheDocument()
+ })
+
it('renders the anchor href', () => {
const fileName = 'file.png'
const mockUrl = 'mock-url'
+ const originalCreateObjectURL = global.URL.createObjectURL
global.URL.createObjectURL = jest.fn().mockReturnValueOnce(mockUrl)
render(
@@ -384,6 +451,8 @@ describe('Value.Upload', () => {
fileName
) as HTMLAnchorElement
expect(anchorElement.href).toMatch(mockUrl)
+
+ global.URL.createObjectURL = originalCreateObjectURL
})
it('renders the download attribute', () => {
diff --git a/packages/dnb-eufemia/src/extensions/forms/Value/Upload/__tests__/__image_snapshots__/valueupload-have-to-match-files-as-non-clickable.snap.png b/packages/dnb-eufemia/src/extensions/forms/Value/Upload/__tests__/__image_snapshots__/valueupload-have-to-match-files-as-non-clickable.snap.png
new file mode 100644
index 00000000000..8adad6e159f
Binary files /dev/null and b/packages/dnb-eufemia/src/extensions/forms/Value/Upload/__tests__/__image_snapshots__/valueupload-have-to-match-files-as-non-clickable.snap.png differ
diff --git a/packages/dnb-eufemia/src/extensions/forms/Value/Upload/__tests__/__image_snapshots__/valueupload-have-to-match-label-and-value-with-on-file-click.snap.png b/packages/dnb-eufemia/src/extensions/forms/Value/Upload/__tests__/__image_snapshots__/valueupload-have-to-match-label-and-value-with-on-file-click.snap.png
new file mode 100644
index 00000000000..b923af14503
Binary files /dev/null and b/packages/dnb-eufemia/src/extensions/forms/Value/Upload/__tests__/__image_snapshots__/valueupload-have-to-match-label-and-value-with-on-file-click.snap.png differ
diff --git a/packages/dnb-eufemia/src/extensions/forms/Value/Upload/__tests__/__image_snapshots__/valueupload-have-to-match-label-and-value.snap.png b/packages/dnb-eufemia/src/extensions/forms/Value/Upload/__tests__/__image_snapshots__/valueupload-have-to-match-label-and-value.snap.png
new file mode 100644
index 00000000000..e44bbe49183
Binary files /dev/null and b/packages/dnb-eufemia/src/extensions/forms/Value/Upload/__tests__/__image_snapshots__/valueupload-have-to-match-label-and-value.snap.png differ
diff --git a/packages/dnb-eufemia/src/extensions/forms/Value/Upload/__tests__/__image_snapshots__/valueupload-have-to-list-upload-inline.snap.png b/packages/dnb-eufemia/src/extensions/forms/Value/Upload/__tests__/__image_snapshots__/valueupload-have-to-match-list-upload-inline.snap.png
similarity index 100%
rename from packages/dnb-eufemia/src/extensions/forms/Value/Upload/__tests__/__image_snapshots__/valueupload-have-to-list-upload-inline.snap.png
rename to packages/dnb-eufemia/src/extensions/forms/Value/Upload/__tests__/__image_snapshots__/valueupload-have-to-match-list-upload-inline.snap.png
diff --git a/packages/dnb-eufemia/src/extensions/forms/Value/Upload/__tests__/__image_snapshots__/valueupload-have-to-match-list-with-on-file-click.snap.png b/packages/dnb-eufemia/src/extensions/forms/Value/Upload/__tests__/__image_snapshots__/valueupload-have-to-match-list-with-on-file-click.snap.png
new file mode 100644
index 00000000000..53e3428ac04
Binary files /dev/null and b/packages/dnb-eufemia/src/extensions/forms/Value/Upload/__tests__/__image_snapshots__/valueupload-have-to-match-list-with-on-file-click.snap.png differ
diff --git a/packages/dnb-eufemia/src/extensions/forms/Value/Upload/__tests__/__image_snapshots__/valueupload-have-to-list-upload-value.snap.png b/packages/dnb-eufemia/src/extensions/forms/Value/Upload/__tests__/__image_snapshots__/valueupload-have-to-match-list.snap.png
similarity index 100%
rename from packages/dnb-eufemia/src/extensions/forms/Value/Upload/__tests__/__image_snapshots__/valueupload-have-to-list-upload-value.snap.png
rename to packages/dnb-eufemia/src/extensions/forms/Value/Upload/__tests__/__image_snapshots__/valueupload-have-to-match-list.snap.png
diff --git a/packages/dnb-eufemia/src/extensions/forms/Value/Upload/stories/Upload.stories.tsx b/packages/dnb-eufemia/src/extensions/forms/Value/Upload/stories/Upload.stories.tsx
index 51ba2b143b9..98bcb20caa5 100644
--- a/packages/dnb-eufemia/src/extensions/forms/Value/Upload/stories/Upload.stories.tsx
+++ b/packages/dnb-eufemia/src/extensions/forms/Value/Upload/stories/Upload.stories.tsx
@@ -1,3 +1,4 @@
+import { createRequest } from '../../../Form/Handler/stories/FormHandler.stories'
import { Form, Value } from '../../..'
import { P } from '../../../../../elements'
@@ -15,13 +16,26 @@ function createMockFile(name: string, size: number, type: string) {
return file
}
+async function mockAsyncFileFetching({ fileItem }) {
+ const request = createRequest()
+ console.log(
+ 'making API request to fetch the url of the file: ' +
+ fileItem.file.name
+ )
+ await request(3000) // Simulate a request
+ window.open(
+ 'https://eufemia.dnb.no/images/avatars/' + fileItem.file.name,
+ '_blank'
+ )
+}
+
export function Upload() {
return (
-
+
-
- layout="grid"
- label.toUpperCase()}
- >
-
-
-
-
-
-
-
- layout="horizontal"
- label.toUpperCase()}
- >
-
-
-
-
-
-
-
- layout="vertical"
- label.toUpperCase()}
- >
-
-
-
-
-
-
-
- empty values
- label.toUpperCase()}
- >
-
-
-
-
-
-
-
+ {
+ file: createMockFile('fileName-4.png', 4000000, 'image/png'),
+ exists: false,
+ id: '4',
+ },
+ ]}
+ />
+
+
+ layout="grid"
+ label.toUpperCase()}
+ >
+
+
+
+
+
+
+
+ layout="horizontal"
+ label.toUpperCase()}
+ >
+
+
+
+
+
+
+
+ layout="vertical"
+ label.toUpperCase()}
+ >
+
+
+
+
+
+
+
+ empty values
+ label.toUpperCase()}
+ >
+
+
+
+
+
+
+
+ >
)
}
diff --git a/packages/dnb-eufemia/src/extensions/forms/Value/Upload/style/dnb-value-upload.scss b/packages/dnb-eufemia/src/extensions/forms/Value/Upload/style/dnb-value-upload.scss
new file mode 100644
index 00000000000..c4fe43c4456
--- /dev/null
+++ b/packages/dnb-eufemia/src/extensions/forms/Value/Upload/style/dnb-value-upload.scss
@@ -0,0 +1,8 @@
+.dnb-forms-value-upload {
+ &__item {
+ .dnb-progress-indicator {
+ display: inline-flex;
+ vertical-align: middle;
+ }
+ }
+}
diff --git a/packages/dnb-eufemia/src/extensions/forms/Value/Upload/style/index.ts b/packages/dnb-eufemia/src/extensions/forms/Value/Upload/style/index.ts
new file mode 100644
index 00000000000..141dd27767d
--- /dev/null
+++ b/packages/dnb-eufemia/src/extensions/forms/Value/Upload/style/index.ts
@@ -0,0 +1,6 @@
+/**
+ * Web Style Import
+ *
+ */
+
+import './dnb-value-upload.scss'
diff --git a/packages/dnb-eufemia/src/extensions/forms/ValueBlock/style/index.ts b/packages/dnb-eufemia/src/extensions/forms/ValueBlock/style/index.ts
index 783bc7bce9e..0155c21c312 100644
--- a/packages/dnb-eufemia/src/extensions/forms/ValueBlock/style/index.ts
+++ b/packages/dnb-eufemia/src/extensions/forms/ValueBlock/style/index.ts
@@ -3,4 +3,4 @@
*
*/
-import './dnb-value.scss'
+import './dnb-value-block.scss'
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 d2661e96c2a..bd49aceeaad 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
@@ -2639,7 +2639,7 @@ describe('useFieldProps', () => {
it('should call "transformOut" initially when "defaultValue" is given', () => {
const transformOut = jest.fn((v) => v + 1)
const transformIn = jest.fn((v) => v - 1)
- const defaultValue = 1
+ const defaultValue = 2
const { result } = renderHook(
() =>
@@ -2657,6 +2657,7 @@ describe('useFieldProps', () => {
})
expect(result.current.value).toEqual(1)
expect(transformOut).toHaveBeenCalledTimes(1)
+ expect(transformIn).toHaveBeenCalledTimes(3)
})
it('should call "transformIn" and "transformOut" after "fromInput" and "toInput"', () => {
diff --git a/packages/dnb-eufemia/src/extensions/forms/hooks/__tests__/useValueProps.test.tsx b/packages/dnb-eufemia/src/extensions/forms/hooks/__tests__/useValueProps.test.tsx
index b4a69b54cfc..7f941a18690 100644
--- a/packages/dnb-eufemia/src/extensions/forms/hooks/__tests__/useValueProps.test.tsx
+++ b/packages/dnb-eufemia/src/extensions/forms/hooks/__tests__/useValueProps.test.tsx
@@ -14,7 +14,7 @@ describe('useValueProps', () => {
it('should prepare value', () => {
const value = 1
- const transformIn = (value) => value + 1
+ const transformIn = (external: unknown) => Number(external) + 1
const { result } = renderHook(() =>
useValueProps({ value, transformIn })
)
@@ -40,7 +40,7 @@ describe('useValueProps', () => {
it('should prepare value from context', () => {
const path = '/contextValue'
- const transformIn = (value) => value + 1
+ const transformIn = (external: unknown) => Number(external) + 1
const { result } = renderHook(
() => useValueProps({ path, transformIn }),
{
diff --git a/packages/dnb-eufemia/src/extensions/forms/hooks/useFieldProps.ts b/packages/dnb-eufemia/src/extensions/forms/hooks/useFieldProps.ts
index 0cdfbba0333..f40748481d7 100644
--- a/packages/dnb-eufemia/src/extensions/forms/hooks/useFieldProps.ts
+++ b/packages/dnb-eufemia/src/extensions/forms/hooks/useFieldProps.ts
@@ -129,8 +129,8 @@ export default function useFieldProps(
validateInitially,
validateUnchanged,
continuousValidation,
- transformIn = (value: Value) => value,
- transformOut = (value: Value) => value,
+ transformIn = (external: unknown) => external as Value,
+ transformOut = (internal: Value) => internal,
toInput = (value: Value) => value,
fromInput = (value: Value) => value,
toEvent = (value: Value) => value,
@@ -248,16 +248,17 @@ export default function useFieldProps(
defaultValueRef.current = defaultValue
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
- const externalValue =
- transformers.current.transformIn(
- useExternalValue({
- path,
- itemPath,
- value: valueProp,
- transformers,
- emptyValue,
- })
- ) ?? defaultValueRef.current
+ const tmpValue = useExternalValue({
+ path,
+ itemPath,
+ value: valueProp,
+ transformers,
+ emptyValue,
+ })
+ const externalValueDeps = tmpValue
+ const externalValue = transformers.current.transformIn(
+ tmpValue ?? defaultValueRef.current
+ )
// Many variables are kept in refs to avoid triggering unnecessary update loops because updates using
// useEffect depend on them (like the external `value`)
@@ -1761,7 +1762,9 @@ export default function useFieldProps(
valueRef.current = externalValue
externalValueDidChangeRef.current = true
}
- }, [externalValue, hasItemPath])
+
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [externalValueDeps, hasItemPath])
useUpdateEffect(() => {
// Error or removed error for this field from the surrounding data context (by path)
@@ -1770,7 +1773,7 @@ export default function useFieldProps(
validateValue()
forceUpdate()
}
- }, [externalValue]) // Keep "externalValue" in the dependency list, so it will be updated when it changes
+ }, [externalValueDeps]) // Keep "externalValue" in the dependency list, so it will be updated when it changes
useEffect(() => {
// Check against the local error state,
@@ -1813,13 +1816,12 @@ export default function useFieldProps(
// First, look for existing data in the context
const hasValue = pointer.has(data, identifier) || identifier === '/'
- const existingValue = transformers.current.transformIn(
+ const existingValue =
identifier === '/'
? data
: hasValue
? pointer.get(data, identifier)
: undefined
- )
// If no data where found in the dataContext, look for shared data
if (
@@ -1831,9 +1833,7 @@ export default function useFieldProps(
const sharedState = createSharedState(dataContext.id)
const hasValue = pointer.has(sharedState.data, identifier)
if (hasValue) {
- const sharedValue = transformers.current.transformIn(
- pointer.get(sharedState.data, identifier)
- )
+ const sharedValue = pointer.get(sharedState.data, identifier)
if (sharedValue) {
valueToStore = sharedValue as Value
}
@@ -1845,6 +1845,8 @@ export default function useFieldProps(
typeof valueToStore === 'undefined'
if (hasDefaultValue) {
+ // Set the default value if it's not set yet.
+ // This takes precedence over the valueToStore.
valueToStore = defaultValueRef.current
defaultValueRef.current = undefined
}
@@ -1902,10 +1904,21 @@ export default function useFieldProps(
}
// Keep Iterate.Array in sync with the data context
- valueRef.current = existingValue
+ valueRef.current = existingValue as Value
}
}
+ // When an Array or Object is used as the value,
+ // and the field uses transformIn, the instance may have been changed.
+ // The "valueToStore" can be undefined,
+ // we then need to ensure we don't overwrite the existing data context value with "undefined".
+ if (
+ typeof valueToStore === 'undefined' &&
+ typeof existingValue !== 'undefined'
+ ) {
+ valueToStore = existingValue
+ }
+
if (
!skipEqualCheck &&
hasValue &&
@@ -1923,9 +1936,10 @@ export default function useFieldProps(
return // stop here, avoid infinite loop
}
+ const valueIn = transformers.current.transformIn(valueToStore)
const transformedValue = transformers.current.transformOut(
- valueToStore as Value,
- transformers.current.provideAdditionalArgs(valueToStore as Value)
+ valueIn,
+ transformers.current.provideAdditionalArgs(valueIn as Value)
)
if (transformedValue !== valueToStore) {
// When the value got transformed, we want to update the internal value, and avoid an infinite loop
@@ -1981,7 +1995,7 @@ export default function useFieldProps(
[
dataContext.internalDataRef,
dataContext.props?.emptyData,
- externalValue, // ensure to include "externalValue" in order to properly remove errors
+ externalValueDeps, // ensure to include "externalValue" in order to properly remove errors
]
)
diff --git a/packages/dnb-eufemia/src/extensions/forms/hooks/useValueProps.ts b/packages/dnb-eufemia/src/extensions/forms/hooks/useValueProps.ts
index c921241ca31..dcfe9e1bffd 100644
--- a/packages/dnb-eufemia/src/extensions/forms/hooks/useValueProps.ts
+++ b/packages/dnb-eufemia/src/extensions/forms/hooks/useValueProps.ts
@@ -34,9 +34,9 @@ export default function useValueProps<
defaultValue,
inheritVisibility,
inheritLabel,
- transformIn = (value: Value) => value,
- toInput = (value: Value) => value,
- fromExternal = (value: Value) => value,
+ transformIn = (external: unknown) => external as Value,
+ toInput = (internal: Value) => internal,
+ fromExternal = (external: Value) => external,
} = props
const transformers = useRef({
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 e79c8783a8b..07a1bced90e 100644
--- a/packages/dnb-eufemia/src/shared/helpers/__tests__/useSharedState.test.ts
+++ b/packages/dnb-eufemia/src/shared/helpers/__tests__/useSharedState.test.ts
@@ -5,6 +5,7 @@ import {
createSharedState,
SharedStateId,
createReferenceKey,
+ useWeakSharedState,
} from '../useSharedState'
import { createContext } from 'react'
@@ -44,9 +45,7 @@ describe('useSharedState', () => {
const { result } = renderHook(() =>
useSharedState(identifier, { test: 'initial' })
)
- const sharedState = createSharedState(identifier, {
- test: 'initial',
- })
+ const sharedState = createSharedState(identifier)
act(() => {
sharedState.update({ test: 'changed' })
})
@@ -101,13 +100,14 @@ describe('useSharedState', () => {
const { result, unmount } = renderHook(() =>
useSharedState(identifier, { test: 'initial' })
)
- const sharedState = createSharedState(identifier, {
- test: 'initial',
- })
+ const sharedState = createSharedState(identifier)
+
unmount()
+
act(() => {
sharedState.update({ test: 'unmounted' })
})
+
expect(result.current.data).toEqual({ test: 'initial' })
})
@@ -253,6 +253,56 @@ describe('useSharedState', () => {
})
})
+describe('useWeakSharedState', () => {
+ it('should delete the shared state when all components have been unmounted', () => {
+ const identifier = {}
+
+ const { unmount: unmountA } = renderHook(() =>
+ useWeakSharedState(identifier, { test: 'initial' })
+ )
+ const { unmount: unmountB } = renderHook(() =>
+ useWeakSharedState(identifier)
+ )
+
+ const getStateOf = (identifier) => {
+ return createSharedState(identifier).get()
+ }
+
+ expect(getStateOf(identifier)).toEqual({ test: 'initial' })
+ expect(getStateOf(identifier)).toEqual({ test: 'initial' })
+
+ unmountA()
+ unmountB()
+
+ expect(getStateOf(identifier)).toEqual(undefined)
+ expect(getStateOf(identifier)).toEqual(undefined)
+ })
+
+ it('when not using weak, should not delete the shared state when all components have been unmounted', () => {
+ const identifier = {}
+
+ const { unmount: unmountA } = renderHook(() =>
+ useSharedState(identifier, { test: 'initial' })
+ )
+ const { unmount: unmountB } = renderHook(() =>
+ useSharedState(identifier)
+ )
+
+ const getStateOf = (identifier) => {
+ return createSharedState(identifier).get()
+ }
+
+ expect(getStateOf(identifier)).toEqual({ test: 'initial' })
+ expect(getStateOf(identifier)).toEqual({ test: 'initial' })
+
+ unmountA()
+ unmountB()
+
+ expect(getStateOf(identifier)).toEqual({ test: 'initial' })
+ expect(getStateOf(identifier)).toEqual({ test: 'initial' })
+ })
+})
+
describe('createReferenceKey', () => {
it('should return the same object for the same references', () => {
const ref1 = {}
diff --git a/packages/dnb-eufemia/src/shared/helpers/useSharedState.tsx b/packages/dnb-eufemia/src/shared/helpers/useSharedState.tsx
index f0775984e3b..3ae031e51de 100644
--- a/packages/dnb-eufemia/src/shared/helpers/useSharedState.tsx
+++ b/packages/dnb-eufemia/src/shared/helpers/useSharedState.tsx
@@ -19,6 +19,21 @@ export type SharedStateId =
| React.Context
| Record
+/**
+ * The shared state will be deleted when all components have been unmounted.
+ */
+export function useWeakSharedState<
+ Data,
+> /** 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
+) {
+ return useSharedState(id, initialData, onChange, { weak: true })
+}
+
/**
* Custom hook that provides shared state functionality.
*/
@@ -28,7 +43,12 @@ export function useSharedState(
/** 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
+ onChange = null,
+ /** Optional configuration options. */
+ {
+ /** When set to `true`, the shared state will be deleted when all components have been unmounted. */
+ weak = false,
+ } = {}
) {
const [, forceUpdate] = useReducer(() => ({}), {})
const hasMountedRef = useMounted()
@@ -125,8 +145,12 @@ export function useSharedState(
return () => {
sharedState.unsubscribe(forceRerender)
+
+ if (weak && sharedState.subscribersRef.current.length === 0) {
+ sharedState.update(undefined)
+ }
}
- }, [forceRerender, id, onChange, sharedState])
+ }, [forceRerender, id, onChange, sharedState, weak])
useEffect(() => {
// Set the onChange function in case it is not set yet
@@ -153,6 +177,7 @@ export interface SharedStateReturn {
set: (newData: Partial) => void
extend: (newData: Partial, opts?: Options) => void
update: (newData: Partial, opts?: Options) => void
+ subscribersRef?: { current: Subscriber[] }
}
interface SharedStateInstance extends SharedStateReturn {
@@ -185,10 +210,12 @@ export function createSharedState(
} = {}
): SharedStateInstance {
if (!sharedStates.get(id)) {
- let subscribers: Subscriber[] = []
+ const subscribersRef = {
+ current: [] as Subscriber[],
+ }
const sync = (opts: Options = {}) => {
- subscribers.forEach((subscriber) => {
+ subscribersRef.current.forEach((subscriber) => {
const syncNow = opts.preventSyncOfSameInstance
? shouldSync?.(subscriber) !== false
: true
@@ -201,7 +228,8 @@ export function createSharedState(
const get = () => sharedStates.get(id).data
const set = (newData: Partial) => {
- sharedStates.get(id).data = { ...newData }
+ sharedStates.get(id).data =
+ newData === undefined ? undefined : { ...newData }
}
const update = (newData: Partial, opts?: Options) => {
@@ -218,13 +246,15 @@ export function createSharedState(
}
const subscribe = (subscriber: Subscriber) => {
- if (!subscribers.includes(subscriber)) {
- subscribers.push(subscriber)
+ if (!subscribersRef.current.includes(subscriber)) {
+ subscribersRef.current.push(subscriber)
}
}
const unsubscribe = (subscriber: Subscriber) => {
- subscribers = subscribers.filter((sub) => sub !== subscriber)
+ subscribersRef.current = subscribersRef.current.filter(
+ (sub) => sub !== subscriber
+ )
}
sharedStates.set(id, {
@@ -236,6 +266,7 @@ export function createSharedState(
subscribe,
unsubscribe,
hadInitialData: Boolean(initialData),
+ subscribersRef,
} as SharedStateInstance)
if (initialData) {
diff --git a/packages/dnb-eufemia/src/style/dnb-ui-forms.scss b/packages/dnb-eufemia/src/style/dnb-ui-forms.scss
index ada4841510b..a747dc3fd8d 100644
--- a/packages/dnb-eufemia/src/style/dnb-ui-forms.scss
+++ b/packages/dnb-eufemia/src/style/dnb-ui-forms.scss
@@ -22,5 +22,6 @@
@import '../extensions/forms/Form/SubmitIndicator/style/dnb-form-submit-indicator.scss';
@import '../extensions/forms/Iterate/style/dnb-iterate.scss';
@import '../extensions/forms/ValueBlock/style/dnb-value-block.scss';
+@import '../extensions/forms/Value/Upload/style/dnb-value-upload.scss';
@import '../extensions/forms/Wizard/style/dnb-wizard-layout.scss';
@import '../extensions/forms/utils/TestElement/style/dnb-test-element.scss';