diff --git a/packages/dnb-design-system-portal/src/docs/uilib/components/upload/Examples.tsx b/packages/dnb-design-system-portal/src/docs/uilib/components/upload/Examples.tsx index fd62aa24129..a22e1fc40ab 100644 --- a/packages/dnb-design-system-portal/src/docs/uilib/components/upload/Examples.tsx +++ b/packages/dnb-design-system-portal/src/docs/uilib/components/upload/Examples.tsx @@ -44,7 +44,7 @@ export const UploadPrefilledFileList = () => ( errorMessage: 'This is no real file!', }, ]) - }, []) + }, [setFiles]) return } @@ -128,7 +128,7 @@ export const UploadRemoveFile = () => ( reader.readAsDataURL(file) }) - }, [files]) + }, [files, images]) return (
@@ -368,8 +368,15 @@ export const UploadOnFileClick = () => ( { file: createMockFile('1501870.jpg', 123, 'image/png'), }, + { + file: createMockFile( + 'file-name-that-is-very-long-and-has-letters.png', + 123, + 'image/png', + ), + }, ]) - }, []) + }, [setFiles]) async function mockAsyncFileFetching({ fileItem }) { const request = createRequest() @@ -385,13 +392,11 @@ export const UploadOnFileClick = () => ( } return ( - <> - - + ) } diff --git a/packages/dnb-eufemia/src/components/dropdown/stories/Dropdown.stories.tsx b/packages/dnb-eufemia/src/components/dropdown/stories/Dropdown.stories.tsx index b612481f121..e6640c42534 100644 --- a/packages/dnb-eufemia/src/components/dropdown/stories/Dropdown.stories.tsx +++ b/packages/dnb-eufemia/src/components/dropdown/stories/Dropdown.stories.tsx @@ -1006,6 +1006,17 @@ export const GlobalStatusExample = () => { ) } +export const TypesExample = () => { + interface MyInterface { + content: string + selected_key: string + } + + const myData: MyInterface[] = [] + + return +} + export function InDialog() { const list = Array(30).fill('Content') return ( diff --git a/packages/dnb-eufemia/src/components/upload/UploadFileListCell.tsx b/packages/dnb-eufemia/src/components/upload/UploadFileListCell.tsx index f01b542aac9..78cd45065d1 100644 --- a/packages/dnb-eufemia/src/components/upload/UploadFileListCell.tsx +++ b/packages/dnb-eufemia/src/components/upload/UploadFileListCell.tsx @@ -1,4 +1,4 @@ -import React, { useRef } from 'react' +import React, { useCallback, useRef } from 'react' import classnames from 'classnames' // Components @@ -25,7 +25,7 @@ import { import { UploadFile, UploadFileNative } from './types' // Shared -import { getPreviousSibling, warn } from '../../shared/component-helper' +import { getPreviousSibling } from '../../shared/component-helper' import useUpload from './useUpload' import { getFileTypeFromExtension } from './UploadVerify' import UploadFileLink from './UploadFileListLink' @@ -96,26 +96,20 @@ const UploadFileListCell = ({ const cellRef = useRef() const exists = useExistsHighlight(id, file) - const handleDisappearFocus = () => { - try { - const cellElement = cellRef.current - const focusElement = getPreviousSibling( - '.dnb-upload', - cellElement - ).querySelector( - '.dnb-upload__file-input-button' - ) as HTMLButtonElement - focusElement.focus() - } catch (e) { - warn(e) - } - } + const handleDisappearFocus = useCallback(() => { + const cellElement = cellRef.current + const focusElement = getPreviousSibling( + '.dnb-upload', + cellElement + )?.querySelector('.dnb-upload__file-input-button') as HTMLButtonElement + focusElement?.focus({ preventScroll: true }) + }, [cellRef]) - const onDeleteHandler = () => { + const onDeleteHandler = useCallback(() => { handleDisappearFocus() onDelete() - } + }, [handleDisappearFocus, onDelete]) return (
  • { it('renders the delete button', () => { render() - const element = screen.getByRole('button') + const element = document.querySelector('button') expect(element).toBeInTheDocument() }) @@ -316,7 +316,7 @@ describe('UploadFileListCell', () => { /> ) - const element = screen.getByRole('button') + const element = document.querySelector('button') expect(element.textContent).toMatch(deleteButtonText) }) @@ -324,7 +324,7 @@ describe('UploadFileListCell', () => { it('renders button as tertiary', () => { render() - const element = screen.getByRole('button') + const element = document.querySelector('button') expect(element.className).toMatch('dnb-button--tertiary') }) @@ -357,7 +357,7 @@ describe('UploadFileListCell', () => { onDelete={onDelete} /> ) - const element = screen.getByRole('button') + const element = document.querySelector('button') fireEvent.click(element) @@ -374,7 +374,7 @@ describe('UploadFileListCell', () => { }} /> ) - const element = screen.getByRole('button') + const element = document.querySelector('button') expect(element).toBeDisabled() }) @@ -394,5 +394,45 @@ describe('UploadFileListCell', () => { document.querySelector('.dnb-progress-indicator') ).not.toBeInTheDocument() }) + + it('should set focus when clicking the delete button', () => { + const MockComponent = () => { + return ( +
    + + +
    + ) + } + const { rerender } = render() + + const removeButton = document.querySelector('button') + const uploadButton = document.querySelector( + '.dnb-upload__file-input-button' + ) + + expect(document.body).toHaveFocus() + + fireEvent.click(removeButton) + expect(uploadButton).toHaveFocus() + + const focus = jest.fn() + jest + .spyOn(HTMLElement.prototype, 'focus') + .mockImplementationOnce(focus) + + rerender() + + fireEvent.click(removeButton) + expect(focus).toHaveBeenCalledTimes(1) + expect(focus).toHaveBeenCalledWith({ preventScroll: true }) + }) }) }) diff --git a/packages/dnb-eufemia/src/components/upload/__tests__/__image_snapshots__/upload-for-sbanken-have-to-match-anchor-looks-when-displaying-a-button.snap.png b/packages/dnb-eufemia/src/components/upload/__tests__/__image_snapshots__/upload-for-sbanken-have-to-match-anchor-looks-when-displaying-a-button.snap.png index 2439b622f99..a3e64fddc35 100644 Binary files a/packages/dnb-eufemia/src/components/upload/__tests__/__image_snapshots__/upload-for-sbanken-have-to-match-anchor-looks-when-displaying-a-button.snap.png and b/packages/dnb-eufemia/src/components/upload/__tests__/__image_snapshots__/upload-for-sbanken-have-to-match-anchor-looks-when-displaying-a-button.snap.png differ diff --git a/packages/dnb-eufemia/src/components/upload/__tests__/__image_snapshots__/upload-for-ui-have-to-match-anchor-looks-when-displaying-a-button.snap.png b/packages/dnb-eufemia/src/components/upload/__tests__/__image_snapshots__/upload-for-ui-have-to-match-anchor-looks-when-displaying-a-button.snap.png index bb5f72a88ea..f6cd1dbda97 100644 Binary files a/packages/dnb-eufemia/src/components/upload/__tests__/__image_snapshots__/upload-for-ui-have-to-match-anchor-looks-when-displaying-a-button.snap.png and b/packages/dnb-eufemia/src/components/upload/__tests__/__image_snapshots__/upload-for-ui-have-to-match-anchor-looks-when-displaying-a-button.snap.png differ diff --git a/packages/dnb-eufemia/src/components/upload/__tests__/__snapshots__/Upload.test.tsx.snap b/packages/dnb-eufemia/src/components/upload/__tests__/__snapshots__/Upload.test.tsx.snap index 077c4f8f28c..fa91facb674 100644 --- a/packages/dnb-eufemia/src/components/upload/__tests__/__snapshots__/Upload.test.tsx.snap +++ b/packages/dnb-eufemia/src/components/upload/__tests__/__snapshots__/Upload.test.tsx.snap @@ -818,13 +818,13 @@ button .dnb-form-status__text { } .dnb-upload__file-cell__content { display: flex; - flex-direction: row; + column-gap: var(--spacing-small); justify-content: space-between; align-items: center; } .dnb-upload__file-cell__content__left { display: flex; - flex-direction: row; + column-gap: var(--spacing-small); align-items: center; } .dnb-upload__file-cell__content__left .dnb-icon { @@ -839,7 +839,6 @@ button .dnb-form-status__text { .dnb-upload__file-cell__text-container { display: flex; flex-direction: column; - margin-left: var(--spacing-small); } .dnb-upload__file-cell__text-container--loading { font-size: var(--font-size-basis); diff --git a/packages/dnb-eufemia/src/components/upload/style/dnb-upload.scss b/packages/dnb-eufemia/src/components/upload/style/dnb-upload.scss index 8f979a744fa..eaa735a06df 100644 --- a/packages/dnb-eufemia/src/components/upload/style/dnb-upload.scss +++ b/packages/dnb-eufemia/src/components/upload/style/dnb-upload.scss @@ -106,14 +106,14 @@ &__content { display: flex; - flex-direction: row; + column-gap: var(--spacing-small); justify-content: space-between; align-items: center; &__left { display: flex; - flex-direction: row; + column-gap: var(--spacing-small); align-items: center; .dnb-icon { @@ -134,8 +134,6 @@ display: flex; flex-direction: column; - margin-left: var(--spacing-small); - &--loading { font-size: var(--font-size-basis); } 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 eceb14fa643..32796efcf1d 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Field/Upload/Upload.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/Field/Upload/Upload.tsx @@ -118,13 +118,13 @@ function UploadComponent(props: Props) { onFileClick, } = rest - const { files: fileContext, setFiles } = useUpload(id) + const { files, setFiles } = useUpload(id) const filesRef = useRef>() useEffect(() => { - filesRef.current = fileContext - }, [fileContext]) + filesRef.current = files + }, [files]) useEffect(() => { // Files stored in session storage will not have a property (due to serialization). @@ -137,7 +137,8 @@ function UploadComponent(props: Props) { const handleChangeAsync = useCallback( async (existingFiles: UploadValue) => { // Filter out existing files - const existingFileIds = fileContext?.map((file) => file.id) || [] + const existingFileIds = + filesRef.current?.map((file) => file.id) || [] const newFiles = existingFiles.filter( (file) => !existingFileIds.includes(file.id) ) @@ -145,7 +146,7 @@ function UploadComponent(props: Props) { if (newFiles.length > 0) { // Set loading setFiles([ - ...fileContext, + ...filesRef.current, ...updateFileLoadingState(newFiles, { isLoading: true }), ]) @@ -171,7 +172,7 @@ function UploadComponent(props: Props) { handleChange(existingFiles) } }, - [fileContext, setFiles, fileHandler, handleChange] + [files, setFiles, fileHandler, handleChange] ) const changeHandler = useCallback( 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 27400b7e807..1dc6d31013a 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 @@ -8,6 +8,7 @@ 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 { wait } from '../../../../../core/jest/jestSetup' const nbForms = nbNOForms['nb-NO'] const nbShared = nbNOShared['nb-NO'] @@ -90,6 +91,32 @@ describe('Field.Upload', () => { expect(onFileClick).toHaveBeenCalledTimes(1) }) + it('should display spinner for an async onFileClick event', async () => { + const onFileClick = jest.fn(async () => { + await wait(1) + }) + + render( + + ) + + const fileButton = document.querySelector( + '.dnb-upload__file-cell button' + ) + + await waitFor(() => { + fireEvent.click(fileButton) + expect( + document.querySelector('.dnb-progress-indicator') + ).toBeInTheDocument() + }) + }) + it('should render files given in data context', () => { render( { }) }) + it('should add new files from fileHandler with async function while removing file', async () => { + const asyncOnFileDelete = jest.fn(async () => { + await wait(1) + }) + + const newFile = (fileId) => { + return createMockFile(`${fileId}.png`, 100, 'image/png') + } + + const filesFirstUpload = [newFile(0)] + + const filesSecondUpload = [newFile(1)] + + const asyncValidatorResolvingWithSuccess = (files) => + new Promise((resolve) => + setTimeout(() => { + const filesToResolve = files.map((file, i) => { + return { + file: file, + id: 'server_generated_id_' + i, + exists: false, + } + }) + resolve(filesToResolve) + }, 1) + ) + + const asyncFileHandlerFnSuccess = jest + .fn(asyncValidatorResolvingWithSuccess) + .mockReturnValueOnce( + asyncValidatorResolvingWithSuccess(filesFirstUpload) + ) + .mockReturnValueOnce( + asyncValidatorResolvingWithSuccess(filesSecondUpload) + ) + + render( + + ) + + const element = getRootElement() + + await waitFor(() => { + // upload the first file + fireEvent.drop(element, { + dataTransfer: { + files: filesFirstUpload, + }, + }) + }) + + await waitFor(() => { + expect( + document.querySelectorAll('.dnb-upload__file-cell').length + ).toBe(1) + }) + + await waitFor(() => { + // upload the second file + fireEvent.drop(element, { + dataTransfer: { + files: filesSecondUpload, + }, + }) + }) + + await waitFor(() => { + expect( + document.querySelectorAll('.dnb-upload__file-cell').length + ).toBe(2) + }) + + await waitFor(() => { + // delete the first file + fireEvent.click( + document + .querySelectorAll('.dnb-upload__file-cell')[0] + .querySelector('button') + ) + }) + + await waitFor(() => { + expect( + document.querySelectorAll('.dnb-upload__file-cell').length + ).toBe(1) + }) + }) + it('should not add existing file using fileHandler with async function', async () => { const file = createMockFile('fileName.png', 100, 'image/png') diff --git a/packages/dnb-eufemia/src/extensions/forms/Tools/Log.tsx b/packages/dnb-eufemia/src/extensions/forms/Tools/Log.tsx index eb3646b4483..271b29ed75c 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Tools/Log.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/Tools/Log.tsx @@ -54,13 +54,15 @@ function replaceUndefinedValues( ): unknown { if (typeof value === 'undefined') { return replaceWith + } else if (Array.isArray(value)) { + return value.map((item) => replaceUndefinedValues(item, replaceWith)) } else if (value && typeof value === 'object' && value !== replaceWith) { return { ...value, ...Object.fromEntries( Object.entries(value).map(([k, v]) => [ k, - replaceUndefinedValues(v), + replaceUndefinedValues(v, replaceWith), ]) ), } diff --git a/packages/dnb-eufemia/src/extensions/forms/Tools/__tests__/Log.test.tsx b/packages/dnb-eufemia/src/extensions/forms/Tools/__tests__/Log.test.tsx new file mode 100644 index 00000000000..a2cd83f9c5a --- /dev/null +++ b/packages/dnb-eufemia/src/extensions/forms/Tools/__tests__/Log.test.tsx @@ -0,0 +1,43 @@ +import React from 'react' +import { render } from '@testing-library/react' +import { Form, Tools } from '../../' + +describe('Tools.Log', () => { + it('should render data context', () => { + const data = { foo: 'bar' } + render( + + + + ) + + const element = document.querySelector('output') + expect(element.textContent).toBe(JSON.stringify(data, null, 2) + ' ') + }) + + it('should format array with square brackets', () => { + const data = { foo: ['bar', 'baz'] } + render( + + + + ) + + const element = document.querySelector('output') + expect(element.textContent).toBe(JSON.stringify(data, null, 2) + ' ') + expect(element.textContent).toContain('[') + expect(element.textContent).toContain('}') + }) + + it('should format "undefined"', () => { + const data = { foo: { bar: undefined } } + render( + + + + ) + + const element = document.querySelector('output') + expect(element.textContent).toContain('"bar": "undefined"') + }) +}) diff --git a/packages/dnb-eufemia/src/fragments/drawer-list/DrawerList.d.ts b/packages/dnb-eufemia/src/fragments/drawer-list/DrawerList.d.ts index cdad59a81cc..071df9ebd98 100644 --- a/packages/dnb-eufemia/src/fragments/drawer-list/DrawerList.d.ts +++ b/packages/dnb-eufemia/src/fragments/drawer-list/DrawerList.d.ts @@ -16,7 +16,7 @@ export type DrawerListValue = string | number; /** @deprecated use `DrawerListDataArrayObject` */ export type DrawerListDataObject = DrawerListDataArrayObject; export type DrawerListDataArrayObject = { - [customProperty: string]: unknown; + [customProperty: string]: any; selected_value?: string | React.ReactNode; selectedKey?: string | number; selected_key?: string | number;