Skip to content

Commit

Permalink
feat: Add File Input component [MDS-1365] (#213)
Browse files Browse the repository at this point in the history
* feat: MDS-1365 file input component - init approach

* feat: MDS-1365 add file upload handler

* feat: MDS-1365 minor changes & minor usage example

* feat: MDS-1365 add changeset

* feat: MDS-1365 minor update

* feat: MDS-1365 add remove file functionality and solve console errors with upload icon

* feat: MDS-1365 use hint instead of alert & add memoise component

* feat: MDS-1365 remove file type from input component prop

* feat: MDS-1365 add back space and enter key behaviour

* feat: MDS-1365 remove console log

* feat: MDS-1365 add file mime type and extension validation

* feat: MDS-1365 add max file size validation

* feat: MDS-1365 fix border and add visual error state

* feat: MDS-1365 minor udpate

* feat: MDS-1365 fix error state style

* feat: MDS-1365 add use imperative handle for better ref managing

* feat: MDS-1365 minor code splitting

* feat: MDS-1365 use useFileInput custom hook for lean code

* feat: MDS-1365 inset file input - init approach

* feat: MDS-1365 create file input base and add it on private components folder

* feat: MDS-1365 inset file input working with error states and focus

* feat: MDS-1365 minor names change

* feat: MDS-1365 renaming file input folder

* feat: MDS-1365 renaming inset file input folder

* feat: MDS-1365 fix label for inset file input

* feat: MDS-1365 minor update

* feat: MDS-1365 use children properly for file input base

* feat: MDS-1365 minor update on label type

* feat: MDS-1365 use file input base on file input component

* feat: MDS-1365 add docs & minor fixes

* feat: test branch

* Prettified Code!

* feat: MDS-1365 improve styles for file input and inset file input in examples page

* feat: MDS-1365 remove hardcoded id & validate init file

* feat: MDS-1365 add max file size default value

* feat: MDS-1378 add border transition

---------

Co-authored-by: MarioGranada <[email protected]>
  • Loading branch information
MarioGranada and MarioGranada authored Dec 2, 2024
1 parent c691086 commit 88fad71
Show file tree
Hide file tree
Showing 20 changed files with 630 additions and 84 deletions.
2 changes: 0 additions & 2 deletions .changeset/gorgeous-cars-jump.md

This file was deleted.

5 changes: 5 additions & 0 deletions .changeset/twelve-rabbits-march.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@heathmont/moon-core-tw": patch
---

feat: Add File Input component [MDS-1365]
113 changes: 70 additions & 43 deletions docs/app/client/input/examples/TextInputTypes.tsx
Original file line number Diff line number Diff line change
@@ -1,52 +1,79 @@
"use client";

import { Input, Label } from "@heathmont/moon-core-tw";
import { FileInput, Input, Label, Hint } from "@heathmont/moon-core-tw";
import { useState } from "react";

const TextInputTypes = () => (
<>
<div className="flex flex-col lg:flex-row justify-around items-end w-full gap-2">
<div className="w-full">
<Label>Number</Label>
<Input type="number" placeholder="e.g. 12345" />
</div>
<div className="w-full">
<Label>Date</Label>
<Input type="date" aria-label="Date" />
</div>
<div className="w-full">
<Label htmlFor="time-type">Time</Label>
<Input type="time" id="time-type" />
</div>
</div>
<div className="flex flex-col lg:flex-row justify-around items-end w-full gap-2">
<div className="w-full">
<Label htmlFor="datetimelocal-type">Datetime local</Label>
<Input type="datetime-local" id="datetimelocal-type" />
</div>
<div className="w-full">
<Label>Email</Label>
<Input type="email" placeholder="e.g. [email protected]" />
</div>
<div className="w-full">
<Label>Password</Label>
<Input type="password" placeholder="Password" />
const TextInputTypes = () => {
const [file, setFile] = useState<File>();
const fileHandler = (file: File | undefined) => {
setFile(file);
};

const removeFileHandler = () => {
setFile(undefined);
};

return (
<>
<div className="flex flex-col lg:flex-row justify-around items-end w-full gap-2">
<div className="w-full">
<Label>Number</Label>
<Input type="number" placeholder="e.g. 12345" />
</div>

<div className="w-full">
<Label>Date</Label>
<Input type="date" aria-label="Date" />
</div>
<div className="w-full">
<Label htmlFor="time-type">Time</Label>
<Input type="time" id="time-type" />
</div>
</div>
</div>
<div className="flex flex-col lg:flex-row justify-around items-end w-full gap-2">
<div className="w-full">
<Label>Search</Label>
<Input type="search" placeholder="e.g. Search something" />
<div className="flex flex-col lg:flex-row justify-around items-end w-full gap-2">
<div className="w-full">
<Label htmlFor="datetimelocal-type">Datetime local</Label>
<Input type="datetime-local" id="datetimelocal-type" />
</div>
<div className="w-full">
<Label>Email</Label>
<Input type="email" placeholder="e.g. [email protected]" />
</div>
<div className="w-full">
<Label>Password</Label>
<Input type="password" placeholder="Password" />
</div>
</div>
<div className="w-full">
<Label>Tel</Label>
<Input type="tel" placeholder="e.g. +372 123 4567" />
<div className="flex flex-col lg:flex-row justify-around items-end w-full gap-2">
<div className="w-full">
<Label>Search</Label>
<Input type="search" placeholder="e.g. Search something" />
</div>
<div className="w-full">
<Label>Tel</Label>
<Input type="tel" placeholder="e.g. +372 123 4567" />
</div>
<div className="w-full">
<Label>Url</Label>
<Input type="url" placeholder="e.g. https://domain.com" />
</div>
</div>
<div className="w-full">
<Label>Url</Label>
<Input type="url" placeholder="e.g. https://domain.com" />
<div className="flex flex-col lg:grid lg:grid-cols-3 gap-2 w-full">
<div>
<Label>File</Label>
<FileInput
id="file-input"
onFileUpload={fileHandler}
onFileRemove={removeFileHandler}
placeholder="Choose a file"
accept=".jpg, .png, video/mp4, .pdf"
maxFileSize={4000 * 1024}
/>
{file && <Hint>File uploaded: {file.name}</Hint>}
</div>
</div>
</div>
</>
);
</>
);
};

export default TextInputTypes;
41 changes: 41 additions & 0 deletions docs/app/client/input/props.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,5 +60,46 @@
"description": "CSS classes for customization"
}
]
},
{
"name": "FileInput",
"props": [
{
"name": "accept",
"type": ["string"],
"description": "Accepted formats. It follows same <input type='file'> accept attribute. See Developer Mozilla docs input file #accept attribute for more information. e.g. image/*,video/*,.pdf",
"defaultState": false
},
{
"name": "maxFileSize",
"type": ["number"],
"description": "Max file size in Bytes",
"defaultState": "100 MB"
},
{
"name": "onFileUpload",
"type": ["(file: File) => void"],
"description": "On File upload callback. It expects only one argument of type File.",
"defaultState": false
},
{
"name": "onFileRemove",
"type": ["() => void"],
"description": "On File remove callback",
"defaultState": false
},
{
"name": "initFile",
"type": ["File"],
"description": "Initial file to display on the input.",
"defaultState": false
},
{
"name": "errorMessages",
"type": ["Errors"],
"description": "Error messages to be displayed. 'maxFileSize' and 'type' only supported currently.",
"defaultState": false
}
]
}
]
101 changes: 63 additions & 38 deletions docs/app/client/insetInput/examples/TextInputTypes.tsx
Original file line number Diff line number Diff line change
@@ -1,43 +1,68 @@
"use client";

import { InsetInput } from "@heathmont/moon-core-tw";
import { InsetFileInput, InsetInput, Hint } from "@heathmont/moon-core-tw";
import { useState } from "react";

const TextInputTypes = () => (
<>
<div className="flex flex-col lg:flex-row justify-around items-end w-full gap-2">
<InsetInput type="number" placeholder="e.g. 12345">
<InsetInput.Label>Number</InsetInput.Label>
</InsetInput>
<InsetInput type="date" aria-label="date">
<InsetInput.Label>Date</InsetInput.Label>
</InsetInput>
<InsetInput type="time" aria-label="time">
<InsetInput.Label>Time</InsetInput.Label>
</InsetInput>
</div>
<div className="flex flex-col lg:flex-row justify-around items-end w-full gap-2">
<InsetInput type="datetime-local" aria-label="datetime-local">
<InsetInput.Label>Datetime local</InsetInput.Label>
</InsetInput>
<InsetInput type="email" placeholder="e.g. [email protected]">
<InsetInput.Label>Email</InsetInput.Label>
</InsetInput>
<InsetInput type="password" placeholder="Password">
<InsetInput.Label>Password</InsetInput.Label>
</InsetInput>
</div>
<div className="flex flex-col lg:flex-row justify-around items-end w-full gap-2">
<InsetInput type="search" placeholder="e.g. Search something">
<InsetInput.Label>Search</InsetInput.Label>
</InsetInput>
<InsetInput type="tel" placeholder="e.g. +372 123 4567">
<InsetInput.Label>Tel</InsetInput.Label>
</InsetInput>
<InsetInput type="url" placeholder="e.g. https://domain.com">
<InsetInput.Label>Url</InsetInput.Label>
</InsetInput>
</div>
</>
);
const TextInputTypes = () => {
const [file, setFile] = useState<File>();
const fileHandler = (file: File | undefined) => {
setFile(file);
};

const removeFileHandler = () => {
setFile(undefined);
};

return (
<>
<div className="flex flex-col lg:flex-row justify-around items-end w-full gap-2">
<InsetInput type="number" placeholder="e.g. 12345">
<InsetInput.Label>Number</InsetInput.Label>
</InsetInput>
<InsetInput type="date" aria-label="date">
<InsetInput.Label>Date</InsetInput.Label>
</InsetInput>
<InsetInput type="time" aria-label="time">
<InsetInput.Label>Time</InsetInput.Label>
</InsetInput>
</div>
<div className="flex flex-col lg:flex-row justify-around items-end w-full gap-2">
<InsetInput type="datetime-local" aria-label="datetime-local">
<InsetInput.Label>Datetime local</InsetInput.Label>
</InsetInput>
<InsetInput type="email" placeholder="e.g. [email protected]">
<InsetInput.Label>Email</InsetInput.Label>
</InsetInput>
<InsetInput type="password" placeholder="Password">
<InsetInput.Label>Password</InsetInput.Label>
</InsetInput>
</div>
<div className="flex flex-col lg:flex-row justify-around items-end w-full gap-2">
<InsetInput type="search" placeholder="e.g. Search something">
<InsetInput.Label>Search</InsetInput.Label>
</InsetInput>
<InsetInput type="tel" placeholder="e.g. +372 123 4567">
<InsetInput.Label>Tel</InsetInput.Label>
</InsetInput>
<InsetInput type="url" placeholder="e.g. https://domain.com">
<InsetInput.Label>Url</InsetInput.Label>
</InsetInput>
</div>
<div className="flex flex-col lg:grid lg:grid-cols-3 lg:gap-2 w-full">
<div className="w-full">
<InsetFileInput
id="file-input"
onFileUpload={fileHandler}
onFileRemove={removeFileHandler}
label={!file ? "Choose a file" : "File"}
accept="image/*, .pdf"
maxFileSize={4000 * 1024}
/>
{file && <Hint>File uploaded: {file.name}</Hint>}
</div>
</div>
</>
);
};

export default TextInputTypes;
41 changes: 41 additions & 0 deletions docs/app/client/insetInput/props.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,46 @@
"description": "CSS classes for customization"
}
]
},
{
"name": "InsetFileInput",
"props": [
{
"name": "accept",
"type": ["string"],
"description": "Accepted formats. It follows same <input type='file'> accept attribute. See Developer Mozilla docs input file #accept attribute for more information. e.g. image/*,video/*,.pdf",
"defaultState": false
},
{
"name": "maxFileSize",
"type": ["number"],
"description": "Max file size in Bytes",
"defaultState": "100 MB"
},
{
"name": "onFileUpload",
"type": ["(file: File) => void"],
"description": "On File upload callback. It expects only one argument of type File.",
"defaultState": false
},
{
"name": "onFileRemove",
"type": ["() => void"],
"description": "On File remove callback",
"defaultState": false
},
{
"name": "initFile",
"type": ["File"],
"description": "Initial file to display on the input.",
"defaultState": false
},
{
"name": "errorMessages",
"type": ["Errors"],
"description": "Error messages to be displayed. 'maxFileSize' and 'type' only supported currently.",
"defaultState": false
}
]
}
]
52 changes: 52 additions & 0 deletions packages/core/src/fileInput/FileInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import React, { forwardRef, memo } from "react";
import Input from "../input/Input";
import mergeClassnames from "../mergeClassnames/mergeClassnames";
import FileInputProps from "./types/FileInputProps";
import FileInputBase from "../private/components/fileInputBase/FileInputBase";
import FileInputRef from "../private/components/fileInputBase/types/FileInputRef";

const FileInput = memo(
forwardRef<FileInputRef, FileInputProps>((props, ref) => {
const {
id,
onFileUpload,
onFileRemove,
initFile,
placeholder,
className,
accept,
maxFileSize,
errorMessages,
...rest
} = props;

return (
<FileInputBase
id={id}
accept={accept}
maxFileSize={maxFileSize}
initFile={initFile}
onFileUpload={onFileUpload}
onFileRemove={onFileRemove}
errorMessages={errorMessages}
ref={ref}
>
{(file: File | undefined) => (
<Input
type="text"
className={mergeClassnames(
"top-0 start-0 pe-10",
className && className,
)}
placeholder={placeholder}
value={file?.name || ""}
readOnly
{...rest}
/>
)}
</FileInputBase>
);
}),
);

export default FileInput;
7 changes: 7 additions & 0 deletions packages/core/src/fileInput/types/FileInputProps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import InputProps from "../../input/private/types/InputProps";
import FileInputBaseProps from "../../private/components/fileInputBase/types/FileInputBaseProps";

type FileInputProps = Omit<InputProps, "type"> &
Omit<FileInputBaseProps, "children">;

export default FileInputProps;
Loading

0 comments on commit 88fad71

Please sign in to comment.