├── .eslintignore ├── .eslintrc.js ├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── .husky └── pre-commit ├── .storybook ├── main.ts ├── preview-head.html └── preview.ts ├── LICENCE ├── README.md ├── docs ├── .gitignore ├── README.md ├── babel.config.js ├── docs │ ├── api-reference.md │ ├── css.md │ ├── getting-started.md │ ├── inheritance.md │ ├── playground.md │ ├── react-hook-form.md │ └── typescript.md ├── docusaurus.config.ts ├── package-lock.json ├── package.json ├── sidebars.js ├── src │ ├── css │ │ ├── custom.css │ │ └── index.css │ ├── dec.d.ts │ └── pages │ │ ├── index.module.css │ │ └── index.tsx ├── static │ ├── .nojekyll │ └── img │ │ ├── favicon.ico │ │ └── logo.svg └── tsconfig.json ├── package-lock.json ├── package.json ├── src ├── components │ └── Input │ │ ├── Input.styled.ts │ │ └── Input.tsx ├── index.stories.tsx ├── index.test.tsx ├── index.tsx ├── index.types.ts ├── shared │ └── helpers │ │ ├── array.ts │ │ └── file.ts └── vite-env.d.ts ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts /.eslintignore: -------------------------------------------------------------------------------- 1 | vite.config.ts 2 | dist 3 | coverage 4 | docs/build 5 | .eslintrc.js -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | '@viclafouch/eslint-config-viclafouch', 4 | '@viclafouch/eslint-config-viclafouch/react', 5 | '@viclafouch/eslint-config-viclafouch/hooks', 6 | '@viclafouch/eslint-config-viclafouch/typescript', 7 | '@viclafouch/eslint-config-viclafouch/prettier' 8 | ], 9 | parserOptions: { 10 | project: ['./tsconfig.json', './docs/tsconfig.json'] 11 | }, 12 | rules: { 13 | 'react-hooks/exhaustive-deps': [ 14 | 'error', 15 | { 16 | additionalHooks: '(useIsomorphicLayoutEffect)' 17 | } 18 | ], 19 | 'import/no-extraneous-dependencies': [ 20 | 'error', 21 | { 22 | devDependencies: [ 23 | '.storybook/**', 24 | 'stories/**', 25 | '**/*.stories.tsx', 26 | '**/*.test.ts', 27 | '**/*.test.tsx' 28 | ] 29 | } 30 | ] 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | coverage 26 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | pnpm lint -------------------------------------------------------------------------------- /.storybook/main.ts: -------------------------------------------------------------------------------- 1 | import type { StorybookConfig } from '@storybook/react-vite' 2 | 3 | const config: StorybookConfig = { 4 | stories: ['../src/*.stories.@(js|jsx|ts|tsx)'], 5 | 6 | addons: [ 7 | '@storybook/addon-links', 8 | '@storybook/addon-essentials', 9 | '@storybook/addon-interactions' 10 | ], 11 | 12 | framework: { 13 | name: '@storybook/react-vite', 14 | options: {} 15 | }, 16 | 17 | docs: {}, 18 | 19 | typescript: { 20 | reactDocgen: 'react-docgen-typescript' 21 | } 22 | } 23 | 24 | export default config 25 | -------------------------------------------------------------------------------- /.storybook/preview-head.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.storybook/preview.ts: -------------------------------------------------------------------------------- 1 | import type { Preview } from '@storybook/react' 2 | 3 | const preview: Preview = { 4 | tags: ['autodocs'] 5 | } 6 | 7 | export default preview 8 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Victor de la Fouchardière 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |

Material UI file input

5 |

A file input designed for the React library Material UI

6 |
7 | 8 |
9 | 10 | [![license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/viclafouch/mui-file-input/blob/main/LICENSE) 11 | ![ts](https://badgen.net/badge/Built%20With/TypeScript/blue) 12 | [![npm](https://img.shields.io/npm/v/mui-file-input)](https://www.npmjs.com/package/mui-file-input) 13 | [![CircleCI](https://circleci.com/gh/viclafouch/mui-file-input/tree/main.svg?style=svg)](https://circleci.com/gh/viclafouch/mui-file-input/tree/main) 14 |
15 | 16 | ## Installation 17 | 18 | ``` 19 | // with npm 20 | npm install mui-file-input 21 | 22 | // with yarn 23 | yarn add mui-file-input 24 | ``` 25 | 26 | ## Usage 27 | 28 | ```jsx 29 | import React from 'react' 30 | import { MuiFileInput } from 'mui-file-input' 31 | 32 | const MyComponent = () => { 33 | const [value, setValue] = React.useState(null) 34 | 35 | const handleChange = (newValue) => { 36 | setValue(newValue) 37 | } 38 | 39 | return 40 | } 41 | ``` 42 | 43 | ## Next.js integration 44 | 45 | Learn how to use MUI File Input with [Next.js](https://nextjs.org/). 46 | 47 | Once you have installed `MUI File Input` in your next.js project, it is important to transpile it as it is an ESM package first. 48 | 49 | ```js 50 | /** @type {import('next').NextConfig} */ 51 | const nextConfig = { 52 | transpilePackages: ['mui-file-input'], 53 | // your config 54 | } 55 | 56 | module.exports = nextConfig 57 | ``` 58 | 59 | ## [Documentation](https://viclafouch.github.io/mui-file-input/) 60 | 61 | ## Changelog 62 | 63 | Go to [GitHub Releases](https://github.com/viclafouch/mui-file-input/releases) 64 | 65 | ## TypeScript 66 | 67 | This library comes with TypeScript "typings". If you happen to find any bugs in those, create an issue. 68 | 69 | ### 🐛 Bugs 70 | 71 | Please file an issue for bugs, missing documentation, or unexpected behavior. 72 | 73 | ### 💡 Feature Requests 74 | 75 | Please file an issue to suggest new features. Vote on feature requests by adding 76 | a 👍. This helps maintainers prioritize what to work on. 77 | 78 | ## LICENSE 79 | 80 | MIT 81 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | /node_modules 3 | 4 | # Production 5 | /build 6 | 7 | # Generated files 8 | .docusaurus 9 | .cache-loader 10 | 11 | # Misc 12 | .DS_Store 13 | .env.local 14 | .env.development.local 15 | .env.test.local 16 | .env.production.local 17 | 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Website 2 | 3 | This website is built using [Docusaurus 2](https://docusaurus.io/), a modern static website generator. 4 | 5 | ### Installation 6 | 7 | ``` 8 | $ yarn 9 | ``` 10 | 11 | ### Local Development 12 | 13 | ``` 14 | $ yarn start 15 | ``` 16 | 17 | This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server. 18 | 19 | ### Build 20 | 21 | ``` 22 | $ yarn build 23 | ``` 24 | 25 | This command generates static content into the `build` directory and can be served using any static contents hosting service. 26 | 27 | ### Deployment 28 | 29 | Using SSH: 30 | 31 | ``` 32 | $ USE_SSH=true yarn deploy 33 | ``` 34 | 35 | Not using SSH: 36 | 37 | ``` 38 | $ GIT_USER= yarn deploy 39 | ``` 40 | 41 | If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch. 42 | -------------------------------------------------------------------------------- /docs/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [require.resolve('@docusaurus/core/lib/babel/preset')] 3 | } 4 | -------------------------------------------------------------------------------- /docs/docs/api-reference.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 3 3 | --- 4 | 5 | # API Reference 6 | 7 | This article discusses the API and props of **MuiFileInput**. Props are defined within `MuiFileInputProps`. 8 | 9 | ## `value` 10 | 11 | - Type: `File` | `null` | `undefined` 12 | - or if `multiple` is present: `File[]` | `undefined` 13 | - Default: `undefined` 14 | 15 | ### Example 16 | 17 | ```tsx 18 | const file = new File(["foo"], "foo.txt", { 19 | type: "text/plain", 20 | }); 21 | 22 | 23 | 24 | 25 | ``` 26 | 27 | ## `onChange` 28 | 29 | - Type: `(value: File | null) => void` 30 | - or if `multiple` is present: `(value: File[]) => void` 31 | 32 | Gets called once the user updates the file value. 33 | 34 | Example: 35 | 36 | ```tsx 37 | 38 | const handleChange = (value) => {} 39 | 40 | 41 | ``` 42 | 43 | ## `inputProps => accept` 44 | 45 | - Type: `string | undefined` 46 | - Default: `undefined` 47 | 48 | Like the native `accept` attribute, when present, it specifies that the user is allowed to enter (`png`, `jpeg`, videos, `pdf`..). 49 | Check here for more info : https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/accept 50 | 51 | ```tsx 52 | // TS will throw an error if the value is a single File instead of an array of Files. 53 | 54 | 55 | 56 | ``` 57 | 58 | ## `multiple` 59 | 60 | - Type: `boolean` 61 | - Default: `false` 62 | 63 | Like the native `multiple` attribute, when present, it specifies that the user is allowed to enter more than one value in the `` element. 64 | The type of the `value` prop will be `File[]` instead of `File`. 65 | 66 | ```tsx 67 | // TS will throw an error if the value is a single File instead of an array of Files. 68 | 69 | ``` 70 | 71 | ## `hideSizeText` 72 | 73 | - Type: `boolean` 74 | - Default: `false` 75 | 76 | In case you do not want to display the size of the current value. 77 | 78 | ```tsx 79 | 80 | ``` 81 | 82 | ## `getInputText` 83 | 84 | - Type: `(value: File | null) => string` 85 | - or if `multiple` is present: `(value: File[]) => string` 86 | - Default: `undefined` 87 | 88 | Customize the render text inside the TextField. 89 | 90 | ```tsx 91 | value ? 'Thanks!' : ''} /> 92 | ``` 93 | 94 | ## `getSizeText` 95 | 96 | - Type: `(value: File | null) => string` 97 | - or if `multiple` is present: `(value: File[]) => string` 98 | - Default: `undefined` 99 | 100 | Customize the render text inside the size Typography. 101 | 102 | ```tsx 103 | 'Very big'} /> 104 | ``` 105 | 106 | ## `clearIconButtonProps` 107 | 108 | - Type: `IconButtonProps` 109 | - Default: `undefined` 110 | 111 | Override the clear IconButton and add a MUI icon. 112 | 113 | Check here to check out all IconButtonProps : https://mui.com/material-ui/api/icon-button/ 114 | 115 | ⚠ You have to install [@mui/icons-material](https://www.npmjs.com/package/@mui/icons-material) library yourself. 116 | 117 | ```tsx 118 | import CloseIcon from '@mui/icons-material/Close' 119 | //... 120 | 121 | 125 | }} 126 | /> 127 | ``` 128 | -------------------------------------------------------------------------------- /docs/docs/css.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 5 3 | --- 4 | 5 | # CSS 6 | 7 | Like any component, if you want to override a component's styles using custom classes, you can use the `className` prop. 8 | 9 | ```jsx 10 | 11 | ``` 12 | 13 | Then, you can use the differents global class names (see below) to target an element of `MuiFileInput`. 14 | 15 | | Global class | Description | 16 | | ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | 17 | | `.MuiFileInput-TextField` | Styles applied to the root element. | 18 | | `.MuiFileInput-Typography-size-text` | Styles applied to the size [Typography](https://mui.com/material-ui/api/typography/). | 19 | | `.MuiFileInput-ClearIconButton` | Styles applied to to the clear [IconButton](https://mui.com/material-ui/api/icon-button/) component. | 20 | | `.MuiFileInput-placeholder` | Styles applied to the placeholder. | 21 | 22 | For example: target the `.MuiFileInput-Typography-size-text` global class name to customize the size text. 23 | 24 | ## Example with styled-component / emotion 25 | 26 | ```jsx 27 | import { styled } from 'styled-component' // or emotion 28 | import { MuiFileInput } from 'mui-file-input' 29 | 30 | const MuiFileInputStyled = styled(MuiFileInput)` 31 | & input + span { 32 | color: red; 33 | } 34 | ` 35 | 36 | function MyComponent() { 37 | return 38 | } 39 | ``` -------------------------------------------------------------------------------- /docs/docs/getting-started.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | --- 4 | 5 | # Getting Started 6 | 7 | ## Install 8 | ```bash 9 | npm install mui-file-input --save 10 | ``` 11 | or you can use **yarn** 12 | ```bash 13 | yarn add mui-file-input 14 | ``` 15 | 16 | We have completed installing the package. 17 | 18 | ## Simple usage 19 | 20 | Here is a simple usage for using the component: 21 | 22 | ```jsx 23 | import React from 'react' 24 | import { MuiFileInput } from 'mui-file-input' 25 | 26 | const MyComponent = () => { 27 | const [file, setFile] = React.useState(null) 28 | 29 | const handleChange = (newFile) => { 30 | setFile(newFile) 31 | } 32 | 33 | return ( 34 | 35 | ) 36 | } 37 | ``` 38 | 39 | ## Next.js integration 40 | 41 | Learn how to use MUI File Input with [Next.js](https://nextjs.org/). 42 | 43 | Once you have installed `MUI File Input` in your next.js project, it is important to transpile it as it is an ESM package first. 44 | 45 | ```js 46 | /** @type {import('next').NextConfig} */ 47 | const nextConfig = { 48 | transpilePackages: ['mui-file-input'], 49 | // your config 50 | } 51 | 52 | module.exports = nextConfig 53 | ``` 54 | 55 | ## Congratulations ! 56 | 57 | That's all, now let's deep dive into the [props](/docs/api-reference). -------------------------------------------------------------------------------- /docs/docs/inheritance.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 4 3 | --- 4 | 5 | # TextField inheritance 6 | 7 | While not explicitly documented, the props of the MUI **[TextField](https://mui.com/api/text-field)** component are also available on the **MuiFileInput** component. 8 | 9 | See: https://mui.com/material-ui/api/text-field/ 10 | 11 | ### Example 12 | 13 | ```jsx 14 | import AttachFileIcon from '@mui/icons-material/AttachFile' 15 | // ... 16 | 17 | 26 | }} 27 | /> 28 | ``` 29 | -------------------------------------------------------------------------------- /docs/docs/playground.md: -------------------------------------------------------------------------------- 1 | # Playground 2 | 3 | Need to play around with **MuiFileInput** in a live environment before deciding if it's the right fit? No problem. 4 | 5 | [![Open in CodeSandbox](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/mui-file-input-t9epbm?fontsize=14&hidenavigation=1&theme=dark) -------------------------------------------------------------------------------- /docs/docs/react-hook-form.md: -------------------------------------------------------------------------------- 1 | # React Hook Form 2 | 3 | Here an example if you want to plug `MuiFileInput` to your form using [React Hook Form](https://react-hook-form.com/). 4 | 5 | ```tsx 6 | import React from "react"; 7 | import ReactDOM from "react-dom"; 8 | import Button from "@mui/material/Button"; 9 | import { MuiFileInput } from "mui-file-input"; 10 | import { Controller, useForm } from "react-hook-form"; 11 | 12 | const App = () => { 13 | const { control, handleSubmit } = useForm({ 14 | defaultValues: { 15 | file: undefined 16 | } 17 | }); 18 | 19 | const onSubmit = (data) => { 20 | alert(JSON.stringify(data)); 21 | }; 22 | 23 | return ( 24 |
25 | ( 29 | 34 | )} 35 | /> 36 |
37 | 40 |
41 | 42 | ) 43 | } 44 | ``` 45 | 46 | [![Edit on CodeSandbox](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/react-hook-form-with-mui-file-input-llrkce?fontsize=14&hidenavigation=1&theme=dark) -------------------------------------------------------------------------------- /docs/docs/typescript.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 4 3 | --- 4 | 5 | # TypeScript 6 | 7 | This package is written in **TypeScript**. So you don't need to create your own types. Here an example if you use **TypeScript**. 8 | 9 | **Nota bene**: Props are defined within the `MuiFileInputProps` interface. 10 | 11 | ```tsx 12 | import React from 'react' 13 | import { MuiFileInput } from 'mui-file-input' 14 | 15 | const MyComponent = () => { 16 | const [value, setValue] = React.useState(null) 17 | 18 | const handleChange = (newValue: File | null) => { 19 | setValue(newValue) 20 | } 21 | 22 | return ( 23 | 28 | ) 29 | } 30 | ``` 31 | -------------------------------------------------------------------------------- /docs/docusaurus.config.ts: -------------------------------------------------------------------------------- 1 | import { themes } from 'prism-react-renderer' 2 | import type * as Preset from '@docusaurus/preset-classic' 3 | import type { Config } from '@docusaurus/types' 4 | 5 | const config = { 6 | title: 'MUI file input', 7 | tagline: 'A file input designed for the React library MUI', 8 | url: 'https://viclafouch.github.io', 9 | baseUrl: '/mui-file-input/', 10 | onBrokenLinks: 'throw', 11 | onBrokenMarkdownLinks: 'warn', 12 | favicon: 'img/favicon.ico', 13 | 14 | // GitHub pages deployment config. 15 | // If you aren't using GitHub pages, you don't need these. 16 | organizationName: 'viclafouch', // Usually your GitHub org/user name. 17 | projectName: 'mui-file-input', // Usually your repo name. 18 | deploymentBranch: 'gh-pages', 19 | trailingSlash: true, 20 | 21 | // Even if you don't use internalization, you can use this field to set useful 22 | // metadata like html gitlang. For example, if your site is Chinese, you may want 23 | // to replace "en" with "zh-Hans". 24 | i18n: { 25 | defaultLocale: 'en', 26 | locales: ['en'] 27 | }, 28 | 29 | presets: [ 30 | [ 31 | 'classic', 32 | { 33 | theme: { 34 | customCss: require.resolve('./src/css/custom.css') 35 | }, 36 | docs: { 37 | sidebarPath: require.resolve('./sidebars.js') 38 | } 39 | } satisfies Preset.Options 40 | ] 41 | ], 42 | 43 | themeConfig: { 44 | colorMode: { 45 | defaultMode: 'dark', 46 | disableSwitch: false, 47 | respectPrefersColorScheme: false 48 | }, 49 | navbar: { 50 | title: 'MUI file input', 51 | logo: { 52 | alt: 'MUI file input', 53 | src: 'img/logo.svg' 54 | }, 55 | items: [ 56 | { 57 | type: 'doc', 58 | docId: 'getting-started', 59 | position: 'left', 60 | label: 'Documentation' 61 | }, 62 | { 63 | href: 'https://github.com/viclafouch/mui-file-input', 64 | label: 'GitHub', 65 | position: 'right' 66 | }, 67 | { 68 | href: 'https://www.npmjs.com/package/mui-file-input', 69 | label: 'NPM', 70 | position: 'right' 71 | } 72 | ] 73 | }, 74 | footer: { 75 | style: 'dark', 76 | copyright: `Copyright © ${new Date().getFullYear()} by Victor de la Fouchardiere` 77 | }, 78 | prism: { 79 | theme: themes.github, 80 | darkTheme: themes.dracula 81 | } 82 | } 83 | } satisfies Config 84 | 85 | module.exports = config 86 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mui-file-input", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "docusaurus": "docusaurus", 7 | "start": "docusaurus start", 8 | "build": "docusaurus build", 9 | "swizzle": "docusaurus swizzle", 10 | "deploy": "docusaurus deploy", 11 | "clear": "docusaurus clear", 12 | "serve": "docusaurus serve", 13 | "write-translations": "docusaurus write-translations", 14 | "write-heading-ids": "docusaurus write-heading-ids", 15 | "typecheck": "tsc" 16 | }, 17 | "dependencies": { 18 | "@docusaurus/core": "3.7.0", 19 | "@docusaurus/preset-classic": "3.7.0", 20 | "@docusaurus/tsconfig": "^3.7.0", 21 | "@mdx-js/react": "^3.1.0", 22 | "@mui/icons-material": "^6.4.7", 23 | "clsx": "^2.1.1", 24 | "mui-file-input": "^7.0.0", 25 | "prism-react-renderer": "^2.4.1", 26 | "react": "^19.0.0", 27 | "react-dom": "^19.0.0" 28 | }, 29 | "devDependencies": { 30 | "@docusaurus/module-type-aliases": "^3.7.0", 31 | "@docusaurus/tsconfig": "^3.7.0", 32 | "typescript": "^5.8.2" 33 | }, 34 | "browserslist": { 35 | "production": [ 36 | ">0.5%", 37 | "not dead", 38 | "not op_mini all" 39 | ], 40 | "development": [ 41 | "last 1 chrome version", 42 | "last 1 firefox version", 43 | "last 1 safari version" 44 | ] 45 | }, 46 | "engines": { 47 | "node": ">=16.14" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /docs/sidebars.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Creating a sidebar enables you to: 3 | - create an ordered group of docs 4 | - render a sidebar for each doc of that group 5 | - provide next/previous navigation 6 | 7 | The sidebars can be generated from the filesystem, or explicitly defined here. 8 | 9 | Create as many sidebars as you want. 10 | */ 11 | 12 | // @ts-check 13 | 14 | /** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */ 15 | const sidebars = { 16 | // By default, Docusaurus generates a sidebar from the docs folder structure 17 | tutorialSidebar: [ 18 | { type: 'autogenerated', dirName: '.' }, 19 | { 20 | type: 'category', 21 | label: 'Related projects', 22 | items: [ 23 | { 24 | type: 'link', 25 | label: 'MUI color input', 26 | href: 'https://viclafouch.github.io/mui-color-input/' 27 | }, 28 | { 29 | type: 'link', 30 | label: 'MUI tel input', 31 | href: 'https://viclafouch.github.io/mui-tel-input/' 32 | }, 33 | { 34 | type: 'link', 35 | label: 'MUI chips input', 36 | href: 'https://viclafouch.github.io/mui-chips-input/' 37 | }, 38 | { 39 | type: 'link', 40 | label: 'MUI OTP input', 41 | href: 'https://viclafouch.github.io/mui-otp-input/' 42 | } 43 | ] 44 | } 45 | ] 46 | 47 | // But you can create a sidebar manually 48 | /* 49 | tutorialSidebar: [ 50 | { 51 | type: 'category', 52 | label: 'Tutorial', 53 | items: ['hello'], 54 | }, 55 | ], 56 | */ 57 | } 58 | 59 | module.exports = sidebars 60 | -------------------------------------------------------------------------------- /docs/src/css/custom.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Any CSS included here will be global. The classic template 3 | * bundles Infima by default. Infima is a CSS framework designed to 4 | * work well for content-centric websites. 5 | */ 6 | 7 | /* You can override the default Infima variables here. */ 8 | :root { 9 | --ifm-code-font-size: 95%; 10 | --ifm-navbar-background-color: #ffffff; 11 | --ifm-color-primary: #000000; 12 | --ifm-navbar-link-hover-color: #000000; 13 | --ifm-breadcrumb-color-active: #000000; 14 | --ifm-menu-color-active: #000000; 15 | --ifm-background-color: #ffffff; 16 | --ifm-link-color: #0072E5; 17 | 18 | --custom-ifm-heading-1: #007FFF; 19 | } 20 | 21 | html[data-theme='dark'] { 22 | --ifm-navbar-background-color: #081A2E; 23 | --ifm-color-primary: #ffffff; 24 | --ifm-toc-link-color: rgba(255, 255, 255, 0.6); 25 | --ifm-navbar-link-hover-color: #f0f0f0; 26 | --ifm-breadcrumb-color-active: #ffffff; 27 | --ifm-menu-color-active: #ffffff; 28 | --ifm-background-color: #011E3C; 29 | --ifm-link-color: #66b2ff; 30 | 31 | --custom-ifm-heading-1: #ffffff; 32 | } 33 | 34 | h1 { 35 | color: var(--custom-ifm-heading-1); 36 | } 37 | 38 | [data-theme='dark'] .footer--dark { 39 | --ifm-footer-background-color: #081A2E; 40 | } 41 | 42 | .hero--primary { 43 | --ifm-hero-background-color: #ffffff; 44 | } 45 | 46 | [data-theme='dark'] .hero--primary { 47 | --ifm-hero-background-color: #011E3C; 48 | } 49 | 50 | [data-theme='dark'] .footer--dark { 51 | --ifm-footer-background-color: #081A2E; 52 | } -------------------------------------------------------------------------------- /docs/src/css/index.css: -------------------------------------------------------------------------------- 1 | .main-wrapper { 2 | display: flex; 3 | } 4 | 5 | 6 | .MuiFileInput-TextField input + span, 7 | .MuiFileInput-TextField .MuiFileInput-Typography-size-text, 8 | .MuiFileInput-TextField svg { 9 | color: var(--ifm-color-black); 10 | } 11 | 12 | [data-theme="dark"] .MuiFileInput-TextField input + span, 13 | [data-theme="dark"] .MuiFileInput-TextField .MuiFileInput-Typography-size-text, 14 | [data-theme="dark"] .MuiFileInput-TextField svg { 15 | color: var(--ifm-color-white); 16 | } 17 | 18 | .MuiFileInput-TextField fieldset { 19 | border-color: var(--ifm-color-black)!important; 20 | } 21 | 22 | [data-theme="dark"] .MuiFileInput-TextField fieldset { 23 | border-color: var(--ifm-color-white)!important; 24 | } -------------------------------------------------------------------------------- /docs/src/dec.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.jpg' 2 | -------------------------------------------------------------------------------- /docs/src/pages/index.module.css: -------------------------------------------------------------------------------- 1 | /** 2 | * CSS files with the .module.css suffix will be treated as CSS modules 3 | * and scoped locally. 4 | */ 5 | 6 | .heroBanner { 7 | padding: 4rem 0; 8 | text-align: center; 9 | position: relative; 10 | overflow: hidden; 11 | color: rgb(178, 186, 194); 12 | flex: 1; 13 | } 14 | 15 | .heroBanner h1 { 16 | margin-top: 20px; 17 | } 18 | 19 | .subtitle { 20 | max-width: 800px; 21 | margin-inline: auto; 22 | } 23 | 24 | @media screen and (max-width: 996px) { 25 | .heroBanner { 26 | padding: 2rem; 27 | } 28 | } 29 | 30 | .buttons { 31 | display: flex; 32 | align-items: center; 33 | justify-content: center; 34 | margin-top: 40px; 35 | } 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /docs/src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | import React from 'react' 3 | import clsx from 'clsx' 4 | import { MuiFileInput } from 'mui-file-input' 5 | import Link from '@docusaurus/Link' 6 | import useDocusaurusContext from '@docusaurus/useDocusaurusContext' 7 | import AttachFileIcon from '@mui/icons-material/AttachFile' 8 | import CloseIcon from '@mui/icons-material/Close' 9 | import DocusaurusImageUrl from '@site/static/img/logo.svg' 10 | import Layout from '@theme/Layout' 11 | import styles from './index.module.css' 12 | import '../css/index.css' 13 | 14 | const HomepageHeader = () => { 15 | const { siteConfig } = useDocusaurusContext() 16 | const [files, setFiles] = React.useState([]) 17 | 18 | const handleChange = (newFiles: File[]) => { 19 | setFiles(newFiles) 20 | } 21 | 22 | return ( 23 |
24 |
25 | 26 |

{siteConfig.title}

27 |

28 | {siteConfig.tagline} 29 |

30 | 37 | }} 38 | InputProps={{ 39 | startAdornment: 40 | }} 41 | /> 42 |
43 | 47 | Get started 48 | 49 |
50 |
51 |
52 | ) 53 | } 54 | 55 | const Home = () => { 56 | const { siteConfig } = useDocusaurusContext() 57 | 58 | return ( 59 | 60 | 61 | 62 | ) 63 | } 64 | 65 | export default Home 66 | -------------------------------------------------------------------------------- /docs/static/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viclafouch/mui-file-input/b80ec8033bf01195f4941ab59d3f01caef3c3415/docs/static/.nojekyll -------------------------------------------------------------------------------- /docs/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viclafouch/mui-file-input/b80ec8033bf01195f4941ab59d3f01caef3c3415/docs/static/img/favicon.ico -------------------------------------------------------------------------------- /docs/static/img/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 20 | 21 | 22 | 23 | 24 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@docusaurus/tsconfig", 3 | "compilerOptions": { 4 | "baseUrl": "." 5 | } 6 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mui-file-input", 3 | "description": "A file input designed for the React library MUI", 4 | "author": "Victor de la Fouchardiere (https://github.com/viclafouch)", 5 | "license": "MIT", 6 | "bugs": { 7 | "url": "https://github.com/viclafouch/mui-file-input/issues" 8 | }, 9 | "homepage": "https://viclafouch.github.io/mui-file-input", 10 | "version": "7.0.0", 11 | "files": [ 12 | "dist" 13 | ], 14 | "main": "./dist/mui-file-input.es.js", 15 | "types": "./dist/index.d.ts", 16 | "exports": { 17 | ".": { 18 | "import": "./dist/mui-file-input.es.js", 19 | "types": "./dist/index.d.ts", 20 | "default": "./dist/mui-file-input.es.js" 21 | } 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "git+https://github.com/viclafouch/mui-file-input.git" 26 | }, 27 | "keywords": [ 28 | "react", 29 | "typescript", 30 | "input", 31 | "mui", 32 | "javascript", 33 | "material", 34 | "ui", 35 | "form", 36 | "file" 37 | ], 38 | "scripts": { 39 | "build": "npm run lint && npm run test -- run && vite build", 40 | "lint": "npx tsc --noEmit && eslint . --ext .js,.jsx,.ts,.tsx", 41 | "lint:fix": "npm lint -- --fix", 42 | "storybook": "storybook dev -p 6006", 43 | "build-storybook": "storybook build", 44 | "test": "vitest", 45 | "release": "standard-version", 46 | "coverage": "vitest run --coverage", 47 | "prepare": "husky" 48 | }, 49 | "standard-version": { 50 | "scripts": { 51 | "prerelease": "npm run build" 52 | }, 53 | "skip": { 54 | "changelog": true 55 | } 56 | }, 57 | "peerDependencies": { 58 | "@emotion/react": "^11.13.0", 59 | "@emotion/styled": "^11.13.0", 60 | "@mui/material": "^6.0.0", 61 | "@types/react": "^18.0.0 || ^19.0.0", 62 | "react": "^18.0.0 || ^19.0.0", 63 | "react-dom": "^18.0.0 || ^19.0.0" 64 | }, 65 | "peerDependenciesMeta": { 66 | "@types/react": { 67 | "optional": true 68 | } 69 | }, 70 | "dependencies": { 71 | "pretty-bytes": "^6.1.1" 72 | }, 73 | "devDependencies": { 74 | "@babel/core": "^7.26.9", 75 | "@emotion/react": "^11.14.0", 76 | "@emotion/styled": "^11.14.0", 77 | "@mui/icons-material": "^6.4.7", 78 | "@mui/material": "^6.4.7", 79 | "@storybook/addon-actions": "^8.6.4", 80 | "@storybook/addon-essentials": "^8.6.4", 81 | "@storybook/addon-interactions": "^8.6.4", 82 | "@storybook/addon-links": "^8.6.4", 83 | "@storybook/react": "^8.6.4", 84 | "@storybook/react-vite": "^8.6.4", 85 | "@storybook/test": "^8.6.4", 86 | "@testing-library/jest-dom": "^6.6.3", 87 | "@testing-library/react": "^16.2.0", 88 | "@testing-library/user-event": "^14.6.1", 89 | "@types/node": "^22.13.10", 90 | "@types/react": "^19.0.10", 91 | "@types/react-dom": "^19.0.4", 92 | "@viclafouch/eslint-config-viclafouch": "4.15.0", 93 | "@vitejs/plugin-react": "^4.3.4", 94 | "axe-core": "^4.10.3", 95 | "babel-loader": "^10.0.0", 96 | "eslint": "^8.56.0", 97 | "husky": "^9.1.7", 98 | "jsdom": "^26.0.0", 99 | "prettier": "^3.5.3", 100 | "react": "^19.0.0", 101 | "react-dom": "^19.0.0", 102 | "rollup-plugin-peer-deps-external": "^2.2.4", 103 | "standard-version": "^9.5.0", 104 | "storybook": "^8.6.4", 105 | "typescript": "^5.5.4", 106 | "vite": "^6.2.1", 107 | "vite-plugin-dts": "^4.5.3", 108 | "vitest": "^3.0.8" 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/components/Input/Input.styled.ts: -------------------------------------------------------------------------------- 1 | import { styled } from '@mui/material/styles' 2 | 3 | const Label = styled('label')` 4 | position: relative; 5 | flex-grow: 1; 6 | 7 | input { 8 | opacity: 0 !important; 9 | } 10 | 11 | & > span { 12 | position: absolute; 13 | left: 0; 14 | right: 0; 15 | top: 0; 16 | bottom: 0; 17 | z-index: 2; 18 | display: flex; 19 | align-items: center; 20 | } 21 | 22 | span.MuiFileInput-placeholder { 23 | color: gray; 24 | } 25 | ` 26 | 27 | const Filename = styled('div')` 28 | display: flex; 29 | width: 100%; 30 | 31 | & > span { 32 | display: block; 33 | } 34 | 35 | & > span:first-of-type { 36 | white-space: nowrap; 37 | text-overflow: ellipsis; 38 | overflow: hidden; 39 | } 40 | 41 | & > span:last-of-type { 42 | flex-shrink: 0; 43 | display: block; 44 | } 45 | ` 46 | 47 | export default { 48 | Label, 49 | Filename 50 | } 51 | -------------------------------------------------------------------------------- /src/components/Input/Input.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Styled from './Input.styled' 3 | 4 | type Toto = React.ComponentProps<'input'> & { 5 | text: string | { filename: string; extension: string } 6 | isPlaceholder: boolean 7 | } 8 | 9 | const Input = ( 10 | { text, isPlaceholder, placeholder, ...restInputProps }: Toto, 11 | ref: React.ForwardedRef 12 | ) => { 13 | return ( 14 | 15 | 16 | {text ? ( 17 | 21 | {typeof text === 'string' ? ( 22 | text 23 | ) : ( 24 | 25 | {text.filename} 26 | .{text.extension} 27 | 28 | )} 29 | 30 | ) : null} 31 | 32 | ) 33 | } 34 | 35 | export default React.forwardRef(Input) 36 | -------------------------------------------------------------------------------- /src/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import AttachFileIcon from '@mui/icons-material/AttachFile' 3 | import CloseIcon from '@mui/icons-material/Close' 4 | import { createTheme, ThemeProvider } from '@mui/material' 5 | import { Meta, StoryFn } from '@storybook/react' 6 | import { MuiFileInput } from './index' 7 | 8 | export default { 9 | title: 'MuiFileInput', 10 | component: MuiFileInput 11 | } as Meta 12 | 13 | const theme = createTheme() 14 | 15 | export const Primary: StoryFn = () => { 16 | const [value, setValue] = React.useState([]) 17 | 18 | const handleChange = (newValue: File[]) => { 19 | setValue(newValue) 20 | } 21 | 22 | return ( 23 | 24 | 29 | }} 30 | InputProps={{ 31 | startAdornment: 32 | }} 33 | required 34 | multiple 35 | value={value} 36 | onChange={handleChange} 37 | label="Your photo" 38 | /> 39 | 40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /src/index.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import axe from 'axe-core' 3 | import { describe, test } from 'vitest' 4 | import { render } from '@testing-library/react' 5 | import { MuiFileInput } from './index' 6 | import '@testing-library/jest-dom/vitest' 7 | 8 | describe('components/MuiFileInput', () => { 9 | test('should not crash', () => { 10 | render() 11 | }) 12 | 13 | test('should meet accessibility standard WCAG 2.2AAA', async () => { 14 | const { container } = render() 15 | const results = await axe.run(container, { 16 | runOnly: { 17 | type: 'tag', 18 | values: [ 19 | 'wcag2a', 20 | 'wcag2aa', 21 | 'wcag2aaa', 22 | 'wcag21a', 23 | 'wcag21aa', 24 | 'wcag21aaa', 25 | 'wcag22a', 26 | 'wcag22aa', 27 | 'wcag22aaa' 28 | ] 29 | } 30 | }) 31 | expect(results.violations.length).toBe(0) 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import prettyBytes from 'pretty-bytes' 3 | import Input from '@components/Input/Input' 4 | import { matchIsNonEmptyArray } from '@shared/helpers/array' 5 | import { 6 | fileListToArray, 7 | getFileDetails, 8 | getTotalFilesSize, 9 | matchIsFile 10 | } from '@shared/helpers/file' 11 | import IconButton from '@mui/material/IconButton' 12 | import InputAdornment from '@mui/material/InputAdornment' 13 | import TextField from '@mui/material/TextField' 14 | import Typography from '@mui/material/Typography' 15 | import type { MuiFileInputProps } from './index.types' 16 | 17 | export { MuiFileInputProps } 18 | 19 | const useIsomorphicLayoutEffect = 20 | typeof window !== 'undefined' ? React.useLayoutEffect : React.useEffect 21 | 22 | export const MuiFileInput = (props: MuiFileInputProps) => { 23 | const { 24 | value, 25 | onChange, 26 | disabled, 27 | getInputText, 28 | getSizeText, 29 | placeholder, 30 | hideSizeText, 31 | ref, 32 | inputProps, 33 | InputProps, 34 | multiple, 35 | className, 36 | clearIconButtonProps = {}, 37 | ...restTextFieldProps 38 | } = props 39 | const { className: iconButtonClassName = '', ...restClearIconButtonProps } = 40 | clearIconButtonProps 41 | const inputRef = React.useRef(null) 42 | const { startAdornment, ...restInputProps } = InputProps || {} 43 | const isMultiple = 44 | multiple || 45 | (inputProps?.multiple as boolean) || 46 | (InputProps?.inputProps?.multiple as boolean) || 47 | false 48 | 49 | const resetInputValue = () => { 50 | if (inputRef.current) { 51 | inputRef.current.value = '' 52 | } 53 | } 54 | 55 | const handleChange = (event: React.ChangeEvent) => { 56 | const fileList = event.target.files 57 | const files = fileList ? fileListToArray(fileList) : [] 58 | 59 | if (multiple) { 60 | onChange?.(files) 61 | 62 | if (files.length === 0) { 63 | resetInputValue() 64 | } 65 | } else { 66 | onChange?.(files[0] || null) 67 | 68 | if (!files[0]) { 69 | resetInputValue() 70 | } 71 | } 72 | } 73 | 74 | const handleClearAll = (event: React.MouseEvent) => { 75 | event.preventDefault() 76 | 77 | if (disabled) { 78 | return 79 | } 80 | 81 | if (multiple) { 82 | onChange?.([]) 83 | } else { 84 | onChange?.(null) 85 | } 86 | } 87 | 88 | const hasAtLeastOneFile = Array.isArray(value) 89 | ? matchIsNonEmptyArray(value) 90 | : matchIsFile(value) 91 | 92 | useIsomorphicLayoutEffect(() => { 93 | const inputElement = inputRef.current 94 | 95 | if (inputElement && !hasAtLeastOneFile) { 96 | inputElement.value = '' 97 | } 98 | }, [hasAtLeastOneFile]) 99 | 100 | const getTheInputText = () => { 101 | if (value === null || (Array.isArray(value) && value.length === 0)) { 102 | return placeholder || '' 103 | } 104 | 105 | if (typeof getInputText === 'function' && value !== undefined) { 106 | return getInputText(value as File & File[]) 107 | } 108 | 109 | if (value && hasAtLeastOneFile) { 110 | if (Array.isArray(value) && value.length > 1) { 111 | return `${value.length} files` 112 | } 113 | 114 | return getFileDetails(value) 115 | } 116 | 117 | return '' 118 | } 119 | 120 | const getTotalSizeText = (): string => { 121 | if (typeof getSizeText === 'function' && value !== undefined) { 122 | return getSizeText(value as File & File[]) 123 | } 124 | 125 | if (hasAtLeastOneFile) { 126 | if (Array.isArray(value)) { 127 | const totalSize = getTotalFilesSize(value) 128 | 129 | return prettyBytes(totalSize) 130 | } 131 | 132 | if (matchIsFile(value)) { 133 | return prettyBytes(value.size) 134 | } 135 | } 136 | 137 | return '' 138 | } 139 | 140 | return ( 141 | {startAdornment} 150 | ), 151 | endAdornment: ( 152 | 156 | {!hideSizeText ? ( 157 | 163 | {getTotalSizeText()} 164 | 165 | ) : null} 166 | 175 | 176 | ), 177 | ...restInputProps, 178 | inputProps: { 179 | text: getTheInputText(), 180 | multiple: isMultiple, 181 | ref: inputRef, 182 | isPlaceholder: !hasAtLeastOneFile, 183 | placeholder, 184 | ...inputProps, 185 | ...InputProps?.inputProps 186 | }, 187 | // @ts-expect-error 188 | inputComponent: Input 189 | }} 190 | {...restTextFieldProps} 191 | /> 192 | ) 193 | } 194 | -------------------------------------------------------------------------------- /src/index.types.ts: -------------------------------------------------------------------------------- 1 | import type { IconButtonProps } from '@mui/material/IconButton' 2 | import type { TextFieldProps as MuiTextFieldProps } from '@mui/material/TextField' 3 | 4 | type TextFieldProps = Omit< 5 | MuiTextFieldProps, 6 | 'onChange' | 'select' | 'type' | 'multiline' | 'defaultValue' 7 | > 8 | 9 | type MultipleOrSingleFile = 10 | | { 11 | value?: File | null 12 | getInputText?: (files: File | null) => string 13 | getSizeText?: (files: File | null) => string 14 | onChange?: (value: File | null) => void 15 | multiple?: false | undefined 16 | } 17 | | { 18 | value?: File[] 19 | getInputText?: (files: File[]) => string 20 | getSizeText?: (files: File[]) => string 21 | onChange?: (value: File[]) => void 22 | multiple: true 23 | } 24 | 25 | export type MuiFileInputProps = TextFieldProps & { 26 | hideSizeText?: boolean 27 | clearIconButtonProps?: IconButtonProps 28 | } & MultipleOrSingleFile 29 | -------------------------------------------------------------------------------- /src/shared/helpers/array.ts: -------------------------------------------------------------------------------- 1 | export function matchIsNonEmptyArray(array: T[]): array is [T, ...T[]] { 2 | return array.length > 0 3 | } 4 | -------------------------------------------------------------------------------- /src/shared/helpers/file.ts: -------------------------------------------------------------------------------- 1 | export function getTotalFilesSize(files: File[]): number { 2 | return files.reduce((previousValue, currentFile) => { 3 | return previousValue + currentFile.size 4 | }, 0) 5 | } 6 | 7 | export function matchIsFile(value: unknown): value is File { 8 | // Secure SSR 9 | return typeof window !== 'undefined' && value instanceof File 10 | } 11 | 12 | export function fileListToArray(filelist: FileList): File[] { 13 | return Array.from(filelist) 14 | } 15 | 16 | export function getFileDetails(value: File | File[]) { 17 | const name = matchIsFile(value) ? value.name : value[0]?.name || '' 18 | const parts = name.split('.') 19 | const extension = parts.pop() as string 20 | const filenameWithoutExtension = parts.join('.') 21 | 22 | return { 23 | filename: filenameWithoutExtension, 24 | extension 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": false, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "noUncheckedIndexedAccess": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "module": "ESNext", 14 | "moduleResolution": "Bundler", 15 | "types": ["vitest/globals"], 16 | "resolveJsonModule": true, 17 | "isolatedModules": true, 18 | "noEmit": true, 19 | "jsx": "react-jsx", 20 | "baseUrl": "./src", 21 | "paths": { 22 | "@assets/*": ["assets/*"], 23 | "@shared/*": ["shared/*"], 24 | "@components/*": ["components/*"] 25 | } 26 | }, 27 | "include": ["src/**/*"], 28 | "exclude": ["vite.config.ts", "coverage", "dist"], 29 | "references": [ 30 | { 31 | "path": "./tsconfig.node.json" 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "esnext", 5 | "moduleResolution": "node" 6 | }, 7 | "include": ["vite.config.ts", ".eslintrc.js"] 8 | } 9 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config' 2 | import react from '@vitejs/plugin-react' 3 | import peerDepsExternal from 'rollup-plugin-peer-deps-external' 4 | import dts from 'vite-plugin-dts' 5 | 6 | const path = require('path') 7 | 8 | // https://vitejs.dev/config/ 9 | export default defineConfig({ 10 | test: { 11 | environment: 'jsdom', 12 | globals: true 13 | }, 14 | resolve: { 15 | alias: { 16 | '@assets': path.resolve(__dirname, './src/assets'), 17 | '@shared': path.resolve(__dirname, './src/shared'), 18 | '@components': path.resolve(__dirname, './src/components') 19 | } 20 | }, 21 | build: { 22 | target: 'esnext', 23 | minify: true, 24 | lib: { 25 | formats: ['es'], 26 | entry: path.resolve(__dirname, 'src/index.tsx'), 27 | name: 'mui-file-input', 28 | fileName: format => `mui-file-input.${format}.js` 29 | }, 30 | rollupOptions: { 31 | output: { 32 | sourcemapExcludeSources: true, 33 | globals: { 34 | react: 'React', 35 | '@mui/material/TextField': 'TextField', 36 | '@mui/material/IconButton': 'IconButton', 37 | '@mui/material/Typography': 'Typography', 38 | '@mui/material/styles': 'styles', 39 | '@mui/icons-material/AttachFile': 'AttachFileIcon', 40 | '@mui/icons-material/Close': 'CloseIcon', 41 | '@mui/material/InputAdornment': 'InputAdornment', 42 | 'react/jsx-runtime': 'jsxRuntime', 43 | 'pretty-bytes': 'prettyBytes' 44 | } 45 | } 46 | } 47 | }, 48 | plugins: [ 49 | peerDepsExternal(), 50 | react(), 51 | dts({ rollupTypes: true, exclude: ['/**/*.stories.tsx', '/**/*.test.tsx'] }) 52 | ] 53 | }) 54 | --------------------------------------------------------------------------------