diff --git a/packages/css/src/components/file-input/file-input.scss b/packages/css/src/components/file-input/file-input.scss index 164b5b21c6..673a583a0d 100644 --- a/packages/css/src/components/file-input/file-input.scss +++ b/packages/css/src/components/file-input/file-input.scss @@ -26,6 +26,10 @@ padding-inline: var(--ams-file-input-padding-inline); touch-action: manipulation; + &.ams-file-input--has-preview { + inline-size: 100vw; + } + @include text-rendering; } @@ -67,3 +71,38 @@ box-shadow: var(--ams-file-input-file-selector-button-hover-box-shadow); color: var(--ams-file-input-file-selector-button-hover-color); } + +.ams-file-input__preview { + display: flex; + flex-direction: column; + gap: var(--ams-file-input-preview-gap); + padding-block: var(--ams-file-input-preview-padding-block); +} + +.ams-file-input__file { + display: flex; + flex-direction: row; + font-family: var(--ams-file-input-preview-file-font-family); + font-size: var(--ams-file-input-preview-file-font-size); + font-weight: var(--ams-file-input-preview-file-font-weight); + gap: var(--ams-file-input-preview-file-gap); + line-height: var(--ams-file-input-preview-file-line-height); +} + +.ams-file-input__file-title { + flex: 1; + gap: var(--ams-file-input-preview-file-gap); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.ams-file-input__file-details { + color: var(--ams-file-input-preview-file-details-color); +} + +.ams-file-input__file-preview { + display: grid; + flex: 0 0 50px; + place-items: center; +} diff --git a/packages/react/src/FileInput/FileInput.tsx b/packages/react/src/FileInput/FileInput.tsx index 3df5f8cec0..03c5709551 100644 --- a/packages/react/src/FileInput/FileInput.tsx +++ b/packages/react/src/FileInput/FileInput.tsx @@ -3,16 +3,132 @@ * Copyright Gemeente Amsterdam */ +import { DocumentIcon } from '@amsterdam/design-system-react-icons' import clsx from 'clsx' -import { forwardRef } from 'react' +import { forwardRef, useEffect, useId, useImperativeHandle, useRef, useState } from 'react' import type { ForwardedRef, InputHTMLAttributes } from 'react' +import { Button } from '../Button' +import { Icon } from '../Icon' -export type FileInputProps = InputHTMLAttributes +export type FileInputProps = { + showFiles?: boolean +} & InputHTMLAttributes -export const FileInput = forwardRef( - ({ className, ...restProps }: FileInputProps, ref: ForwardedRef) => ( - - ), +export const FileInput = forwardRef( + ({ showFiles, className, ...restProps }, ref: ForwardedRef) => { + const fileInputId = useId() + const previewRef = useRef(null) + const [files, setFiles] = useState(null) + + const inputRef = useRef(null) + + useImperativeHandle(ref, () => inputRef.current as HTMLInputElement) + + const updateFilesPreview = () => { + if (inputRef.current) { + setFiles(inputRef.current.files) + } + } + + useEffect(() => { + if (showFiles && inputRef.current) { + inputRef.current.addEventListener('change', updateFilesPreview) + return () => { + inputRef.current?.removeEventListener('change', updateFilesPreview) + } + } + return () => window.removeEventListener('change', updateFilesPreview) + }, [showFiles]) + + const prettyBytes = (num: number, precision = 3) => { + const UNITS = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] + + if (num === 0) return '0 B' + + const exponent = Math.floor(Math.log10(num) / 3) + const size = (num / Math.pow(1000, exponent)).toPrecision(precision) + + return `${size}${UNITS[exponent]}` + } + + const prettyType = (type: string) => { + switch (type) { + case 'image/gif': + return 'gif' + case 'image/jpeg': + return 'jpg' + case 'image/png': + return 'png' + case 'application/pdf': + return 'pdf' + case 'application/msword': + return 'Word' + case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': + return 'Word' + case 'application/vnd.ms-excel': + return 'Excel' + case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': + return 'Excel' + case 'application/vnd.ms-powerpoint': + return 'PowerPoint' + case 'application/vnd.openxmlformats-officedocument.presentationml.presentation': + return 'PowerPoint' + default: + return type + } + } + + return ( + <> + + {showFiles && ( +
+ {files && files.length > 0 + ? Array.from(files).map((file) => ( +
+
+ {file.type.includes('image') ? ( + {file.name} + ) : ( + + )} +
+
+ {file.name} +
+ ({prettyType(file.type)}, {prettyBytes(file.size)} ) +
+
+ +
+ )) + : null} +
+ )} + + ) + }, ) FileInput.displayName = 'FileInput' diff --git a/proprietary/tokens/src/components/ams/file-input.tokens.json b/proprietary/tokens/src/components/ams/file-input.tokens.json index c33573e612..022a98d98b 100644 --- a/proprietary/tokens/src/components/ams/file-input.tokens.json +++ b/proprietary/tokens/src/components/ams/file-input.tokens.json @@ -36,6 +36,20 @@ "forced-color-mode": { "border": { "value": "{ams.border.width.md} solid" } } + }, + "preview": { + "gap": { "value": "{ams.space.md}" }, + "padding-block": { "value": "{ams.space.md}" }, + "file": { + "font-family": { "value": "{ams.text.font-family}" }, + "font-size": { "value": "{ams.text.level.6.font-size}" }, + "font-weight": { "value": "{ams.text.font-weight.normal}" }, + "gap": { "value": "{ams.space.sm}" }, + "line-height": { "value": "{ams.text.level.6.line-height}" }, + "details": { + "color": { "value": "{ams.color.neutral-grey3}" } + } + } } } } diff --git a/storybook/src/components/FileInput/FileInput.stories.tsx b/storybook/src/components/FileInput/FileInput.stories.tsx index 405acb119d..361447d96f 100644 --- a/storybook/src/components/FileInput/FileInput.stories.tsx +++ b/storybook/src/components/FileInput/FileInput.stories.tsx @@ -63,3 +63,7 @@ export const InAField: Story = { ), } + +export const WithPreview: Story = { + args: { showFiles: true, multiple: true }, +}