├── .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 | [](https://github.com/viclafouch/mui-file-input/blob/main/LICENSE)
11 | 
12 | [](https://www.npmjs.com/package/mui-file-input)
13 | [](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 | [](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 |
42 | )
43 | }
44 | ```
45 |
46 | [](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 |
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 |
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 |
--------------------------------------------------------------------------------