Design system.
Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.
 
 
 

301 linhas
10 KiB

  1. import * as React from 'react';
  2. import { delegateTriggerEvent, useClientSide } from '@modal-sh/react-utils';
  3. import clsx from 'clsx';
  4. export interface CommonPreviewProps<F extends Partial<File> = Partial<File>> {
  5. file?: F;
  6. disabled?: boolean;
  7. enhanced?: boolean;
  8. mini?: boolean;
  9. }
  10. export type FileSelectBoxDerivedElement = HTMLInputElement;
  11. export interface FileSelectBoxProps<
  12. F extends Partial<File> = Partial<File>,
  13. P extends CommonPreviewProps<F> = CommonPreviewProps<F>
  14. > extends Omit<React.HTMLProps<FileSelectBoxDerivedElement>, 'size' | 'type' | 'style' | 'label' | 'list'> {
  15. /**
  16. * Should the component display a border?
  17. */
  18. border?: boolean,
  19. /**
  20. * Should the component occupy the whole width of its parent?
  21. */
  22. block?: boolean,
  23. /**
  24. * Short textual description indicating the nature of the component's value.
  25. */
  26. label?: React.ReactNode,
  27. /**
  28. * Short textual description as guidelines for valid input values.
  29. */
  30. hint?: React.ReactNode,
  31. enhanced?: boolean,
  32. /**
  33. * Is the label hidden?
  34. */
  35. hiddenLabel?: boolean,
  36. previewComponent: React.ElementType<P>,
  37. }
  38. export const FileSelectBox = React.forwardRef<FileSelectBoxDerivedElement, FileSelectBoxProps>(
  39. (
  40. {
  41. label = '',
  42. hint = '',
  43. border = false,
  44. block = false,
  45. enhanced: enhancedProp = false,
  46. hiddenLabel = false,
  47. multiple = false,
  48. onChange,
  49. disabled = false,
  50. className,
  51. id: idProp,
  52. previewComponent: FilePreviewComponent,
  53. ...etcProps
  54. }: FileSelectBoxProps,
  55. forwardedRef,
  56. ) => {
  57. const { clientSide } = useClientSide({ clientSide: enhancedProp });
  58. const [fileList, setFileList] = React.useState<FileList>();
  59. const [lastUpdated, setLastUpdated] = React.useState<number>();
  60. const defaultRef = React.useRef<HTMLInputElement>(null);
  61. const ref = forwardedRef ?? defaultRef;
  62. const labelId = React.useId();
  63. const defaultId = React.useId();
  64. const id = idProp ?? defaultId;
  65. const doSetFileList: React.ChangeEventHandler<HTMLInputElement> = (e) => {
  66. if (enhancedProp) {
  67. setFileList(e.currentTarget.files as FileList);
  68. setLastUpdated(Date.now());
  69. }
  70. onChange?.(e);
  71. };
  72. const doClearFileList: React.MouseEventHandler<HTMLButtonElement> = (e) => {
  73. e.preventDefault();
  74. if (!(typeof ref === 'object' && ref)) {
  75. return;
  76. }
  77. const { current } = ref;
  78. if (!current) {
  79. return;
  80. }
  81. current.value = '';
  82. setFileList(undefined);
  83. setLastUpdated(Date.now());
  84. setTimeout(() => {
  85. delegateTriggerEvent('change', current);
  86. });
  87. };
  88. const cancelEvent = (e: React.DragEvent) => {
  89. e.stopPropagation();
  90. e.preventDefault();
  91. };
  92. const handleDropZone: React.DragEventHandler<HTMLDivElement> = async (e) => {
  93. cancelEvent(e);
  94. if (!(typeof ref === 'object' && ref)) {
  95. return;
  96. }
  97. const { current } = ref;
  98. if (!current) {
  99. return;
  100. }
  101. const { dataTransfer } = e;
  102. const { files } = dataTransfer;
  103. if (!(files && files.length > 0)) {
  104. return;
  105. }
  106. setFileList(current.files = files);
  107. setLastUpdated(Date.now());
  108. setTimeout(() => {
  109. delegateTriggerEvent('change', current);
  110. });
  111. };
  112. const filesCount = fileList?.length ?? 0;
  113. return (
  114. <div
  115. className={clsx(
  116. 'relative rounded ring-secondary/50 group',
  117. 'focus-within:ring-4',
  118. block && 'w-full',
  119. !block && 'inline-block min-w-64',
  120. className,
  121. )}
  122. onDragEnter={cancelEvent}
  123. onDragOver={cancelEvent}
  124. onDrop={handleDropZone}
  125. data-testid="root"
  126. >
  127. <label
  128. className="block absolute top-0 left-0 w-full h-full cursor-pointer"
  129. data-testid="clickArea"
  130. htmlFor={id}
  131. />
  132. <input
  133. {...etcProps}
  134. id={id}
  135. disabled={disabled}
  136. ref={ref}
  137. type="file"
  138. className={clsx(
  139. 'peer',
  140. {
  141. 'sr-only': clientSide,
  142. }
  143. )}
  144. onChange={doSetFileList}
  145. multiple={multiple}
  146. data-testid="input"
  147. aria-labelledby={label ? `${labelId}` : undefined}
  148. />
  149. {
  150. label && (
  151. <div
  152. data-testid="label"
  153. id={labelId}
  154. className={clsx(
  155. 'absolute z-[1] w-full top-0.5 left-0 pointer-events-none pl-1 text-xxs font-bold peer-disabled:opacity-50 group-focus-within:text-secondary text-primary leading-none bg-negative',
  156. {
  157. 'sr-only': hiddenLabel,
  158. },
  159. )}
  160. >
  161. <div className="w-full whitespace-nowrap h-[1.1em] overflow-hidden text-ellipsis">
  162. {label}
  163. </div>
  164. </div>
  165. )
  166. }
  167. {
  168. filesCount < 1
  169. && clientSide
  170. && hint
  171. && (
  172. <div
  173. data-testid="hint"
  174. className="absolute top-0 left-0 w-full h-full pointer-events-none box-border overflow-hidden pt-4"
  175. >
  176. <div className="flex items-center justify-center w-full h-full">
  177. {hint}
  178. </div>
  179. </div>
  180. )
  181. }
  182. {
  183. filesCount > 0
  184. && clientSide
  185. && (
  186. <React.Fragment key={lastUpdated}>
  187. <div className={`sm:absolute top-0 left-0 w-full h-full pointer-events-none pb-12 box-border overflow-hidden pt-8`}>
  188. <div
  189. className={`pointer-events-auto w-full h-full px-4 pb-4 box-border`}
  190. >
  191. {
  192. multiple
  193. && (
  194. <div className="w-full h-full overflow-auto -mx-4 px-4">
  195. <div className="w-full grid gap-4 grid-cols-3">
  196. {Array.from(fileList ?? []).map((file, i) => {
  197. return (
  198. <div
  199. key={`${file.name}:${i}`}
  200. data-testid="selectedFileItem"
  201. className={`w-full aspect-square rounded overflow-hidden relative before:absolute before:content-[''] before:bg-current before:top-0 before:left-0 before:w-full before:h-full before:opacity-10`}
  202. >
  203. <FilePreviewComponent
  204. file={file}
  205. enhanced={clientSide}
  206. disabled={disabled}
  207. mini
  208. />
  209. </div>
  210. );
  211. })}
  212. </div>
  213. </div>
  214. )
  215. }
  216. {
  217. !multiple
  218. && Array.from(fileList ?? []).map((file, i) => {
  219. return (
  220. <div
  221. key={`${file.name}:${i}`}
  222. className="w-full h-full"
  223. >
  224. <div
  225. data-testid="selectedFileItem"
  226. className="h-full w-full p-4 box-border rounded overflow-hidden relative before:absolute before:content-[''] before:bg-current before:top-0 before:left-0 before:w-full before:h-full before:opacity-10"
  227. >
  228. <div
  229. className="w-full h-full relative"
  230. >
  231. <FilePreviewComponent
  232. file={file}
  233. enhanced={clientSide}
  234. disabled={disabled}
  235. />
  236. </div>
  237. </div>
  238. </div>
  239. )
  240. })
  241. }
  242. </div>
  243. </div>
  244. <div className="absolute bottom-0 left-0 w-full text-center h-12 box-border flex">
  245. <div className="w-0 flex-auto flex flex-col items-center justify-center h-full">
  246. <label
  247. data-testid="reselect"
  248. htmlFor={id}
  249. className="flex w-full h-full bg-negative text-primary disabled:text-primary focus:text-secondary active:text-tertiary items-center justify-center leading-none gap-4 select-none"
  250. >
  251. <span
  252. className="block uppercase font-bold h-[1.1em] w-full whitespace-nowrap overflow-hidden text-ellipsis font-semi-expanded"
  253. >
  254. Reselect
  255. </span>
  256. </label>
  257. </div>
  258. <div className="w-0 flex-auto flex flex-col items-center justify-center h-full">
  259. <button
  260. data-testid="clear"
  261. type="button"
  262. onClick={doClearFileList}
  263. className="flex w-full h-full bg-negative text-primary disabled:text-primary focus:text-secondary active:text-tertiary items-center justify-center leading-none gap-4 select-none focus:outline-0"
  264. >
  265. <span
  266. className="block uppercase font-bold h-[1.1em] w-full whitespace-nowrap overflow-hidden text-ellipsis font-semi-expanded"
  267. >
  268. Clear
  269. </span>
  270. </button>
  271. </div>
  272. </div>
  273. </React.Fragment>
  274. )
  275. }
  276. {
  277. border && (
  278. <span
  279. data-testid="border"
  280. className="absolute z-[1] peer-disabled:opacity-50 inset-0 rounded-inherit border-2 border-primary pointer-events-none group-focus-within:border-secondary"
  281. />
  282. )
  283. }
  284. </div>
  285. );
  286. }
  287. );
  288. FileSelectBox.displayName = 'FileSelectBox';