@recursica/adapter-mui-v7 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.storybook/commonArgTypes.ts +148 -0
- package/ARCHITECTURE.md +18 -0
- package/CHANGELOG.md +582 -0
- package/LICENSE +21 -0
- package/OVERSTYLING.md +59 -0
- package/README.md +70 -0
- package/SETUP.md +92 -0
- package/USAGE.md +57 -0
- package/dist/adapter-mui-v7.cjs +292 -0
- package/dist/adapter-mui-v7.cjs.map +1 -0
- package/dist/adapter-mui-v7.css +1 -0
- package/dist/adapter-mui-v7.js +40757 -0
- package/dist/adapter-mui-v7.js.map +1 -0
- package/dist/index.d.ts +2350 -0
- package/dist/vite.svg +1 -0
- package/docs/PHILOSOPHY.md +39 -0
- package/llms.txt +76 -0
- package/package.json +151 -0
- package/recursica_brand.json +2677 -0
- package/recursica_tokens.json +799 -0
- package/recursica_ui-kit.json +14981 -0
- package/recursica_variables_scoped.css +30489 -0
- package/src/GlobalExemptions.modules.css +7 -0
- package/src/Introduction.stories.tsx +184 -0
- package/src/OverStyling.tsx +87 -0
- package/src/Version.tsx +37 -0
- package/src/components/Accordion/ACCORDION_IMPLEMENTATION_NOTES.md +107 -0
- package/src/components/Accordion/Accordion.module.css +362 -0
- package/src/components/Accordion/Accordion.stories.tsx +198 -0
- package/src/components/Accordion/Accordion.test.tsx +1 -0
- package/src/components/Accordion/Accordion.tsx +285 -0
- package/src/components/Accordion/USAGE.md +43 -0
- package/src/components/Accordion/index.ts +1 -0
- package/src/components/AssistiveElement/ASSISTIVEELEMENT_IMPLEMENTATION_NOTES.md +59 -0
- package/src/components/AssistiveElement/AssistiveElement.module.css +121 -0
- package/src/components/AssistiveElement/AssistiveElement.stories.tsx +68 -0
- package/src/components/AssistiveElement/AssistiveElement.test.tsx +1 -0
- package/src/components/AssistiveElement/AssistiveElement.tsx +94 -0
- package/src/components/AssistiveElement/USAGE.md +36 -0
- package/src/components/AssistiveElement/index.ts +1 -0
- package/src/components/AutoComplete/AutoComplete.module.css +456 -0
- package/src/components/AutoComplete/AutoComplete.stories.tsx +336 -0
- package/src/components/AutoComplete/AutoComplete.test.tsx +1 -0
- package/src/components/AutoComplete/AutoComplete.tsx +266 -0
- package/src/components/AutoComplete/IMPLEMENTATION_NOTES.md +37 -0
- package/src/components/AutoComplete/USAGE.md +68 -0
- package/src/components/AutoComplete/index.ts +1 -0
- package/src/components/Avatar/AVATAR_IMPLEMENTATION_NOTES.md +36 -0
- package/src/components/Avatar/Avatar.module.css +336 -0
- package/src/components/Avatar/Avatar.stories.tsx +92 -0
- package/src/components/Avatar/Avatar.test.tsx +1 -0
- package/src/components/Avatar/Avatar.tsx +88 -0
- package/src/components/Avatar/USAGE.md +38 -0
- package/src/components/Avatar/index.ts +1 -0
- package/src/components/Badge/Badge.module.css +115 -0
- package/src/components/Badge/Badge.stories.tsx +78 -0
- package/src/components/Badge/Badge.test.tsx +1 -0
- package/src/components/Badge/Badge.tsx +61 -0
- package/src/components/Badge/USAGE.md +40 -0
- package/src/components/Badge/index.ts +1 -0
- package/src/components/Breadcrumb/BREADCRUMB_IMPLEMENTATION_NOTES.md +51 -0
- package/src/components/Breadcrumb/Breadcrumb.module.css +51 -0
- package/src/components/Breadcrumb/Breadcrumb.stories.tsx +114 -0
- package/src/components/Breadcrumb/Breadcrumb.test.tsx +1 -0
- package/src/components/Breadcrumb/Breadcrumb.tsx +70 -0
- package/src/components/Breadcrumb/USAGE.md +42 -0
- package/src/components/Breadcrumb/index.ts +1 -0
- package/src/components/Button/BUTTON_IMPLEMENTATION_NOTES.md +123 -0
- package/src/components/Button/Button.module.css +413 -0
- package/src/components/Button/Button.stories.tsx +196 -0
- package/src/components/Button/Button.styleIsolation.dom.test.tsx +98 -0
- package/src/components/Button/Button.tsx +156 -0
- package/src/components/Button/USAGE.md +46 -0
- package/src/components/Button/index.ts +1 -0
- package/src/components/Card/CARD_IMPLEMENTATION_NOTES.md +15 -0
- package/src/components/Card/Card.module.css +155 -0
- package/src/components/Card/Card.stories.tsx +131 -0
- package/src/components/Card/Card.test.tsx +1 -0
- package/src/components/Card/Card.tsx +202 -0
- package/src/components/Card/USAGE.md +57 -0
- package/src/components/Card/index.ts +1 -0
- package/src/components/Checkbox/CHECKBOX_IMPLEMENTATION_NOTES.md +14 -0
- package/src/components/Checkbox/Checkbox.module.css +286 -0
- package/src/components/Checkbox/Checkbox.stories.tsx +97 -0
- package/src/components/Checkbox/Checkbox.test.tsx +1 -0
- package/src/components/Checkbox/Checkbox.tsx +259 -0
- package/src/components/Checkbox/CheckboxGroup.stories.tsx +139 -0
- package/src/components/Checkbox/CheckboxGroup.tsx +148 -0
- package/src/components/Checkbox/USAGE.md +36 -0
- package/src/components/Checkbox/index.ts +6 -0
- package/src/components/Chip/CHIP_IMPLEMENTATION_NOTES.md +102 -0
- package/src/components/Chip/Chip.module.css +283 -0
- package/src/components/Chip/Chip.stories.tsx +154 -0
- package/src/components/Chip/Chip.test.tsx +1 -0
- package/src/components/Chip/Chip.tsx +168 -0
- package/src/components/Chip/USAGE.md +55 -0
- package/src/components/Chip/index.ts +1 -0
- package/src/components/Container/Container.stories.tsx +118 -0
- package/src/components/Container/Container.test.tsx +1 -0
- package/src/components/Container/Container.tsx +51 -0
- package/src/components/Container/IMPLEMENTATION_NOTES.md +5 -0
- package/src/components/Container/USAGE.md +48 -0
- package/src/components/Container/index.ts +1 -0
- package/src/components/DatePicker/DATEPICKER_IMPLEMENTATION_NOTES.md +29 -0
- package/src/components/DatePicker/DatePicker.icons.tsx +35 -0
- package/src/components/DatePicker/DatePicker.module.css +464 -0
- package/src/components/DatePicker/DatePicker.stories.tsx +163 -0
- package/src/components/DatePicker/DatePicker.test.tsx +1 -0
- package/src/components/DatePicker/DatePicker.tsx +229 -0
- package/src/components/DatePicker/USAGE.md +49 -0
- package/src/components/DatePicker/index.ts +1 -0
- package/src/components/Dropdown/BareDropdown.tsx +168 -0
- package/src/components/Dropdown/DROPDOWN_IMPLEMENTATION_NOTES.md +45 -0
- package/src/components/Dropdown/Dropdown.icons.tsx +33 -0
- package/src/components/Dropdown/Dropdown.module.css +535 -0
- package/src/components/Dropdown/Dropdown.stories.tsx +252 -0
- package/src/components/Dropdown/Dropdown.test.tsx +1 -0
- package/src/components/Dropdown/Dropdown.tsx +319 -0
- package/src/components/Dropdown/USAGE.md +68 -0
- package/src/components/Dropdown/index.ts +1 -0
- package/src/components/FileInput/FILEINPUT_IMPLEMENTATION_NOTES.md +153 -0
- package/src/components/FileInput/FileInput.module.css +268 -0
- package/src/components/FileInput/FileInput.stories.tsx +313 -0
- package/src/components/FileInput/FileInput.test.tsx +1 -0
- package/src/components/FileInput/FileInput.tsx +415 -0
- package/src/components/FileInput/USAGE.md +145 -0
- package/src/components/FileInput/index.ts +1 -0
- package/src/components/FileUpload/FILEUPLOAD_IMPLEMENTATION_NOTES.md +249 -0
- package/src/components/FileUpload/FileUpload.module.css +200 -0
- package/src/components/FileUpload/FileUpload.stories.tsx +362 -0
- package/src/components/FileUpload/FileUpload.test.tsx +1 -0
- package/src/components/FileUpload/FileUpload.tsx +352 -0
- package/src/components/FileUpload/USAGE.md +199 -0
- package/src/components/FileUpload/index.ts +1 -0
- package/src/components/Flex/Flex.stories.tsx +128 -0
- package/src/components/Flex/Flex.test.tsx +1 -0
- package/src/components/Flex/Flex.tsx +89 -0
- package/src/components/Flex/USAGE.md +41 -0
- package/src/components/Flex/index.ts +1 -0
- package/src/components/FormControlLayout/FormControlLayout.module.css +92 -0
- package/src/components/FormControlLayout/FormControlLayout.stories.tsx +118 -0
- package/src/components/FormControlLayout/FormControlLayout.test.tsx +1 -0
- package/src/components/FormControlLayout/FormControlLayout.tsx +72 -0
- package/src/components/FormControlLayout/USAGE.md +40 -0
- package/src/components/FormControlLayout/index.ts +1 -0
- package/src/components/FormControlWrapper/FORMCONTROLWRAPPER_IMPLEMENTATION_NOTES.md +35 -0
- package/src/components/FormControlWrapper/FormControlWrapper.module.css +87 -0
- package/src/components/FormControlWrapper/FormControlWrapper.stories.tsx +129 -0
- package/src/components/FormControlWrapper/FormControlWrapper.test.tsx +1 -0
- package/src/components/FormControlWrapper/FormControlWrapper.tsx +149 -0
- package/src/components/FormControlWrapper/USAGE.md +40 -0
- package/src/components/FormControlWrapper/index.ts +1 -0
- package/src/components/Grid/Grid.module.css +72 -0
- package/src/components/Grid/Grid.stories.tsx +161 -0
- package/src/components/Grid/Grid.test.tsx +1 -0
- package/src/components/Grid/Grid.tsx +155 -0
- package/src/components/Grid/IMPLEMENTATION_NOTES.md +24 -0
- package/src/components/Grid/USAGE.md +53 -0
- package/src/components/Grid/index.ts +1 -0
- package/src/components/Group/Group.stories.tsx +120 -0
- package/src/components/Group/Group.test.tsx +1 -0
- package/src/components/Group/Group.tsx +116 -0
- package/src/components/Group/USAGE.md +41 -0
- package/src/components/Group/index.ts +1 -0
- package/src/components/Heading/Heading.stories.tsx +50 -0
- package/src/components/Heading/Heading.test.tsx +1 -0
- package/src/components/Heading/Heading.tsx +28 -0
- package/src/components/Heading/USAGE.md +36 -0
- package/src/components/Heading/index.ts +1 -0
- package/src/components/HoverCard/HoverCard.module.css +107 -0
- package/src/components/HoverCard/HoverCard.stories.tsx +172 -0
- package/src/components/HoverCard/HoverCard.test.tsx +1 -0
- package/src/components/HoverCard/HoverCard.tsx +145 -0
- package/src/components/HoverCard/USAGE.md +45 -0
- package/src/components/HoverCard/index.ts +1 -0
- package/src/components/Label/LABEL_IMPLEMENTATION_NOTES.md +27 -0
- package/src/components/Label/Label.module.css +169 -0
- package/src/components/Label/Label.stories.tsx +131 -0
- package/src/components/Label/Label.test.tsx +1 -0
- package/src/components/Label/Label.tsx +96 -0
- package/src/components/Label/USAGE.md +36 -0
- package/src/components/Label/index.ts +1 -0
- package/src/components/Layer/USAGE.md +50 -0
- package/src/components/Link/LINK_IMPLEMENTATION_NOTES.md +9 -0
- package/src/components/Link/Link.module.css +105 -0
- package/src/components/Link/Link.stories.tsx +58 -0
- package/src/components/Link/Link.test.tsx +1 -0
- package/src/components/Link/Link.tsx +45 -0
- package/src/components/Link/USAGE.md +40 -0
- package/src/components/Link/index.ts +1 -0
- package/src/components/Loader/LOADER_IMPLEMENTATION_NOTES.md +25 -0
- package/src/components/Loader/Loader.animate.dom.test.tsx +72 -0
- package/src/components/Loader/Loader.module.css +175 -0
- package/src/components/Loader/Loader.stories.tsx +123 -0
- package/src/components/Loader/Loader.test.tsx +1 -0
- package/src/components/Loader/Loader.tsx +72 -0
- package/src/components/Loader/USAGE.md +42 -0
- package/src/components/Loader/index.ts +1 -0
- package/src/components/Menu/IMPLEMENTATION_NOTES.md +5 -0
- package/src/components/Menu/Menu.module.css +283 -0
- package/src/components/Menu/Menu.stories.tsx +405 -0
- package/src/components/Menu/Menu.test.tsx +1 -0
- package/src/components/Menu/Menu.tsx +103 -0
- package/src/components/Menu/USAGE.md +53 -0
- package/src/components/Menu/index.ts +1 -0
- package/src/components/Modal/MODAL_IMPLEMENTATION_NOTES.md +31 -0
- package/src/components/Modal/Modal.module.css +302 -0
- package/src/components/Modal/Modal.stories.tsx +105 -0
- package/src/components/Modal/Modal.test.tsx +1 -0
- package/src/components/Modal/Modal.tsx +231 -0
- package/src/components/Modal/USAGE.md +40 -0
- package/src/components/Modal/index.ts +1 -0
- package/src/components/NumberInput/NUMBER_INPUT_IMPLEMENTATION_NOTES.md +93 -0
- package/src/components/NumberInput/NumberInput.module.css +303 -0
- package/src/components/NumberInput/NumberInput.stories.tsx +96 -0
- package/src/components/NumberInput/NumberInput.test.tsx +1 -0
- package/src/components/NumberInput/NumberInput.tsx +273 -0
- package/src/components/NumberInput/USAGE.md +36 -0
- package/src/components/NumberInput/index.ts +1 -0
- package/src/components/Pagination/IMPLEMENTATION_NOTES.md +40 -0
- package/src/components/Pagination/Pagination.icons.tsx +66 -0
- package/src/components/Pagination/Pagination.module.css +197 -0
- package/src/components/Pagination/Pagination.stories.tsx +48 -0
- package/src/components/Pagination/Pagination.test.tsx +1 -0
- package/src/components/Pagination/Pagination.tsx +123 -0
- package/src/components/Pagination/USAGE.md +36 -0
- package/src/components/Pagination/index.ts +1 -0
- package/src/components/Panel/PANEL_IMPLEMENTATION_NOTES.md +133 -0
- package/src/components/Panel/Panel.module.css +231 -0
- package/src/components/Panel/Panel.stories.tsx +227 -0
- package/src/components/Panel/Panel.test.tsx +1 -0
- package/src/components/Panel/Panel.tsx +186 -0
- package/src/components/Panel/USAGE.md +46 -0
- package/src/components/Panel/index.ts +1 -0
- package/src/components/Popover/IMPLEMENTATION_NOTES.md +22 -0
- package/src/components/Popover/Popover.module.css +118 -0
- package/src/components/Popover/Popover.stories.tsx +133 -0
- package/src/components/Popover/Popover.test.tsx +1 -0
- package/src/components/Popover/Popover.tsx +267 -0
- package/src/components/Popover/USAGE.md +69 -0
- package/src/components/Popover/index.ts +1 -0
- package/src/components/Radio/RADIO_IMPLEMENTATION_NOTES.md +12 -0
- package/src/components/Radio/Radio.module.css +247 -0
- package/src/components/Radio/Radio.stories.tsx +93 -0
- package/src/components/Radio/Radio.test.tsx +1 -0
- package/src/components/Radio/Radio.tsx +223 -0
- package/src/components/Radio/RadioGroup.stories.tsx +117 -0
- package/src/components/Radio/RadioGroup.tsx +122 -0
- package/src/components/Radio/USAGE.md +36 -0
- package/src/components/Radio/index.ts +2 -0
- package/src/components/ReadOnlyField/ReadOnlyBooleanField.tsx +21 -0
- package/src/components/ReadOnlyField/ReadOnlyField.module.css +54 -0
- package/src/components/ReadOnlyField/ReadOnlyField.stories.tsx +163 -0
- package/src/components/ReadOnlyField/ReadOnlyField.test.tsx +1 -0
- package/src/components/ReadOnlyField/ReadOnlyField.tsx +108 -0
- package/src/components/ReadOnlyField/ReadOnlySwitchField.tsx +21 -0
- package/src/components/ReadOnlyField/ReadOnlyTextField.tsx +48 -0
- package/src/components/ReadOnlyField/USAGE.md +36 -0
- package/src/components/ReadOnlyField/WithReadOnlyWrapper.tsx +93 -0
- package/src/components/ReadOnlyField/index.ts +2 -0
- package/src/components/RecursicaThemeProvider/USAGE.md +60 -0
- package/src/components/SegmentedControl/IMPLEMENTATION_NOTES.md +14 -0
- package/src/components/SegmentedControl/SegmentedControl.module.css +201 -0
- package/src/components/SegmentedControl/SegmentedControl.stories.tsx +106 -0
- package/src/components/SegmentedControl/SegmentedControl.test.tsx +1 -0
- package/src/components/SegmentedControl/SegmentedControl.tsx +149 -0
- package/src/components/SegmentedControl/USAGE.md +49 -0
- package/src/components/SegmentedControl/index.ts +1 -0
- package/src/components/Slider/IMPLEMENTATION_NOTES.md +55 -0
- package/src/components/Slider/Slider.module.css +635 -0
- package/src/components/Slider/Slider.stories.tsx +282 -0
- package/src/components/Slider/Slider.test.tsx +1 -0
- package/src/components/Slider/Slider.tsx +435 -0
- package/src/components/Slider/USAGE.md +56 -0
- package/src/components/Slider/index.ts +1 -0
- package/src/components/Stack/Stack.stories.tsx +108 -0
- package/src/components/Stack/Stack.test.tsx +1 -0
- package/src/components/Stack/Stack.tsx +43 -0
- package/src/components/Stack/USAGE.md +41 -0
- package/src/components/Stack/index.ts +1 -0
- package/src/components/Stepper/IMPLEMENTATION_NOTES.md +120 -0
- package/src/components/Stepper/Stepper.module.css +474 -0
- package/src/components/Stepper/Stepper.stories.tsx +157 -0
- package/src/components/Stepper/Stepper.test.tsx +1 -0
- package/src/components/Stepper/Stepper.tsx +240 -0
- package/src/components/Stepper/USAGE.md +45 -0
- package/src/components/Stepper/index.ts +1 -0
- package/src/components/Switch/SWITCH_IMPLEMENTATION_NOTES.md +147 -0
- package/src/components/Switch/Switch.module.css +346 -0
- package/src/components/Switch/Switch.stories.tsx +114 -0
- package/src/components/Switch/Switch.test.tsx +1 -0
- package/src/components/Switch/Switch.tsx +294 -0
- package/src/components/Switch/SwitchGroup.stories.tsx +132 -0
- package/src/components/Switch/SwitchGroup.tsx +156 -0
- package/src/components/Switch/USAGE.md +36 -0
- package/src/components/Switch/index.ts +2 -0
- package/src/components/Table/TABLE_IMPLEMENTATION_NOTES.md +75 -0
- package/src/components/Table/Table.module.css +462 -0
- package/src/components/Table/Table.stories.tsx +154 -0
- package/src/components/Table/Table.test.tsx +1 -0
- package/src/components/Table/Table.tsx +250 -0
- package/src/components/Table/USAGE.md +90 -0
- package/src/components/Table/index.ts +1 -0
- package/src/components/Tabs/IMPLEMENTATION_NOTES.md +76 -0
- package/src/components/Tabs/Tabs.module.css +801 -0
- package/src/components/Tabs/Tabs.stories.tsx +174 -0
- package/src/components/Tabs/Tabs.test.tsx +1 -0
- package/src/components/Tabs/Tabs.tsx +103 -0
- package/src/components/Tabs/USAGE.md +61 -0
- package/src/components/Tabs/index.ts +1 -0
- package/src/components/Text/TEXT_IMPLEMENTATION_NOTES.md +10 -0
- package/src/components/Text/Text.stories.tsx +75 -0
- package/src/components/Text/Text.test.tsx +1 -0
- package/src/components/Text/Text.tsx +19 -0
- package/src/components/Text/USAGE.md +40 -0
- package/src/components/Text/index.ts +1 -0
- package/src/components/TextArea/TEXTAREA_IMPLEMENTATION_NOTES.md +42 -0
- package/src/components/TextArea/TextArea.module.css +203 -0
- package/src/components/TextArea/TextArea.stories.tsx +94 -0
- package/src/components/TextArea/TextArea.test.tsx +1 -0
- package/src/components/TextArea/TextArea.tsx +162 -0
- package/src/components/TextArea/USAGE.md +38 -0
- package/src/components/TextArea/index.ts +1 -0
- package/src/components/TextField/TextField.module.css +271 -0
- package/src/components/TextField/TextField.stories.tsx +162 -0
- package/src/components/TextField/TextField.test.tsx +1 -0
- package/src/components/TextField/TextField.tsx +182 -0
- package/src/components/TextField/USAGE.md +38 -0
- package/src/components/TextField/index.ts +1 -0
- package/src/components/TimePicker/TIMEPICKER_IMPLEMENTATION_NOTES.md +89 -0
- package/src/components/TimePicker/TimePicker.module.css +262 -0
- package/src/components/TimePicker/TimePicker.stories.tsx +155 -0
- package/src/components/TimePicker/TimePicker.test.tsx +1 -0
- package/src/components/TimePicker/TimePicker.tsx +277 -0
- package/src/components/TimePicker/USAGE.md +61 -0
- package/src/components/TimePicker/index.ts +1 -0
- package/src/components/Timeline/IMPLEMENTATION_NOTES.md +42 -0
- package/src/components/Timeline/Timeline.module.css +363 -0
- package/src/components/Timeline/Timeline.stories.tsx +128 -0
- package/src/components/Timeline/Timeline.test.tsx +1 -0
- package/src/components/Timeline/Timeline.tsx +90 -0
- package/src/components/Timeline/TimelineItem.tsx +95 -0
- package/src/components/Timeline/USAGE.md +41 -0
- package/src/components/Timeline/index.ts +2 -0
- package/src/components/Toast/TOAST_IMPLEMENTATION_NOTES.md +37 -0
- package/src/components/Toast/Toast.module.css +193 -0
- package/src/components/Toast/Toast.stories.tsx +67 -0
- package/src/components/Toast/Toast.test.tsx +1 -0
- package/src/components/Toast/Toast.tsx +121 -0
- package/src/components/Toast/USAGE.md +42 -0
- package/src/components/Toast/index.ts +1 -0
- package/src/components/Tooltip/Tooltip.module.css +83 -0
- package/src/components/Tooltip/Tooltip.stories.tsx +165 -0
- package/src/components/Tooltip/Tooltip.test.tsx +1 -0
- package/src/components/Tooltip/Tooltip.tsx +78 -0
- package/src/components/Tooltip/USAGE.md +40 -0
- package/src/components/Tooltip/index.ts +1 -0
- package/src/components/TransferList/TRANSFERLIST_IMPLEMENTATION_NOTES.md +140 -0
- package/src/components/TransferList/TransferList.module.css +174 -0
- package/src/components/TransferList/TransferList.stories.tsx +121 -0
- package/src/components/TransferList/TransferList.test.tsx +1 -0
- package/src/components/TransferList/TransferList.tsx +419 -0
- package/src/components/TransferList/USAGE.md +77 -0
- package/src/components/TransferList/index.ts +1 -0
- package/src/components/Tree/IMPLEMENTATION_NOTES.md +53 -0
- package/src/components/Tree/Tree.module.css +272 -0
- package/src/components/Tree/Tree.stories.tsx +104 -0
- package/src/components/Tree/Tree.test.tsx +1 -0
- package/src/components/Tree/Tree.tsx +298 -0
- package/src/components/Tree/USAGE.md +99 -0
- package/src/components/Tree/index.ts +1 -0
- package/src/components/Typography/TYPOGRAPHY_IMPLEMENTATION_NOTES.md +17 -0
- package/src/components/Typography/Typography.module.css +13 -0
- package/src/components/Typography/Typography.test.tsx +1 -0
- package/src/components/Typography/Typography.tsx +43 -0
- package/src/components/Typography/USAGE.md +40 -0
- package/src/components/Typography/index.ts +1 -0
- package/src/components/index.ts +50 -0
- package/src/env.d.ts +7 -0
- package/src/index.ts +194 -0
- package/src/types/index.ts +9 -0
- package/src/types/scss-modules.d.ts +9 -0
- package/src/utils/ColorSchemeWrapper.tsx +23 -0
- package/src/utils/RequireAccessibleLabel.ts +1 -0
- package/src/utils/copyToClipboard.ts +36 -0
- package/src/utils/filterStylingProps.test.ts +85 -0
- package/src/utils/filterStylingProps.ts +143 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/renderRichOption.tsx +51 -0
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import React, { useState } from "react";
|
|
3
|
+
import { FileUpload } from "./FileUpload";
|
|
4
|
+
import { type RecursicaFileUploadItem } from "@recursica/adapter-common";
|
|
5
|
+
import { formControlArgTypes } from "../../../.storybook/commonArgTypes";
|
|
6
|
+
|
|
7
|
+
function mockFile(name: string, size = 1024) {
|
|
8
|
+
return new File([new Uint8Array(size)], name);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const meta: Meta<typeof FileUpload> = {
|
|
12
|
+
title: "UI-Kit/FileUpload",
|
|
13
|
+
component: FileUpload,
|
|
14
|
+
tags: ["autodocs"],
|
|
15
|
+
parameters: {
|
|
16
|
+
docs: {
|
|
17
|
+
description: {
|
|
18
|
+
component: `
|
|
19
|
+
The \`FileUpload\` component is a drag-and-drop dropzone with a native browse-button fallback, integrated directly into the \`FormControlWrapper\` architecture. Selected files render as a removable-chip list below the dropzone.
|
|
20
|
+
|
|
21
|
+
### Examples
|
|
22
|
+
\`\`\`tsx
|
|
23
|
+
<FileUpload
|
|
24
|
+
label="Upload Files"
|
|
25
|
+
assistiveText="Max file size 5MB"
|
|
26
|
+
files={files}
|
|
27
|
+
onFilesAdded={(added) =>
|
|
28
|
+
setFiles((prev) => [...prev, ...added.map((file) => ({ file }))])
|
|
29
|
+
}
|
|
30
|
+
onFileRemove={(id) =>
|
|
31
|
+
setFiles((prev) => prev.filter((item) => (item.id ?? item.file.name) !== id))
|
|
32
|
+
}
|
|
33
|
+
/>
|
|
34
|
+
\`\`\`
|
|
35
|
+
`,
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
argTypes: {
|
|
40
|
+
...formControlArgTypes,
|
|
41
|
+
accept: {
|
|
42
|
+
control: "text",
|
|
43
|
+
description:
|
|
44
|
+
"Native `accept` attribute for the file picker (e.g. `.pdf,.png`).",
|
|
45
|
+
},
|
|
46
|
+
multiple: {
|
|
47
|
+
control: "boolean",
|
|
48
|
+
description: "Whether multiple files can be selected/dropped at once.",
|
|
49
|
+
},
|
|
50
|
+
maxSize: {
|
|
51
|
+
control: "number",
|
|
52
|
+
description: "Maximum size per file, in bytes.",
|
|
53
|
+
},
|
|
54
|
+
maxFiles: {
|
|
55
|
+
control: "number",
|
|
56
|
+
description: "Maximum total number of files allowed.",
|
|
57
|
+
},
|
|
58
|
+
readOnly: {
|
|
59
|
+
control: "boolean",
|
|
60
|
+
description:
|
|
61
|
+
"Renders `files` as a static chip list with no remove icon, and hides the dropzone.",
|
|
62
|
+
},
|
|
63
|
+
dropzoneLabel: {
|
|
64
|
+
control: "text",
|
|
65
|
+
},
|
|
66
|
+
browseButtonLabel: {
|
|
67
|
+
control: "text",
|
|
68
|
+
},
|
|
69
|
+
icon: {
|
|
70
|
+
table: { disable: true },
|
|
71
|
+
},
|
|
72
|
+
files: {
|
|
73
|
+
table: { disable: true },
|
|
74
|
+
},
|
|
75
|
+
onFilesAdded: {
|
|
76
|
+
table: { disable: true },
|
|
77
|
+
},
|
|
78
|
+
onFileRemove: {
|
|
79
|
+
table: { disable: true },
|
|
80
|
+
},
|
|
81
|
+
onFilesRejected: {
|
|
82
|
+
table: { disable: true },
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export default meta;
|
|
88
|
+
|
|
89
|
+
type Story = StoryObj<typeof FileUpload>;
|
|
90
|
+
type FileUploadStoryArgs = React.ComponentProps<typeof FileUpload> & {
|
|
91
|
+
withLayer?: boolean;
|
|
92
|
+
layer?: number;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export const Default: Story = {
|
|
96
|
+
args: {
|
|
97
|
+
label: "Upload Files",
|
|
98
|
+
assistiveText: "Max file size 5MB",
|
|
99
|
+
},
|
|
100
|
+
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
101
|
+
render: function FileUploadStory({
|
|
102
|
+
withLayer,
|
|
103
|
+
layer,
|
|
104
|
+
...args
|
|
105
|
+
}: FileUploadStoryArgs) {
|
|
106
|
+
/* eslint-enable @typescript-eslint/no-unused-vars */
|
|
107
|
+
const [files, setFiles] = useState<RecursicaFileUploadItem[]>([]);
|
|
108
|
+
return (
|
|
109
|
+
<FileUpload
|
|
110
|
+
{...args}
|
|
111
|
+
files={files}
|
|
112
|
+
onFilesAdded={(added) =>
|
|
113
|
+
setFiles((prev) => [...prev, ...added.map((file) => ({ file }))])
|
|
114
|
+
}
|
|
115
|
+
onFileRemove={(id) =>
|
|
116
|
+
setFiles((prev) =>
|
|
117
|
+
prev.filter((item) => (item.id ?? item.file.name) !== id),
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
/>
|
|
121
|
+
);
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
export const WithFiles: Story = {
|
|
126
|
+
args: {
|
|
127
|
+
label: "Upload Files",
|
|
128
|
+
assistiveText: "Max file size 5MB",
|
|
129
|
+
},
|
|
130
|
+
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
131
|
+
render: function FileUploadStory({
|
|
132
|
+
withLayer,
|
|
133
|
+
layer,
|
|
134
|
+
...args
|
|
135
|
+
}: FileUploadStoryArgs) {
|
|
136
|
+
/* eslint-enable @typescript-eslint/no-unused-vars */
|
|
137
|
+
const [files, setFiles] = useState<RecursicaFileUploadItem[]>([
|
|
138
|
+
{ file: mockFile("document.pdf") },
|
|
139
|
+
{ file: mockFile("image.png") },
|
|
140
|
+
{ file: mockFile("spreadsheet.xlsx") },
|
|
141
|
+
]);
|
|
142
|
+
return (
|
|
143
|
+
<FileUpload
|
|
144
|
+
{...args}
|
|
145
|
+
files={files}
|
|
146
|
+
onFilesAdded={(added) =>
|
|
147
|
+
setFiles((prev) => [...prev, ...added.map((file) => ({ file }))])
|
|
148
|
+
}
|
|
149
|
+
onFileRemove={(id) =>
|
|
150
|
+
setFiles((prev) =>
|
|
151
|
+
prev.filter((item) => (item.id ?? item.file.name) !== id),
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
/>
|
|
155
|
+
);
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
export const EmptyState: Story = {
|
|
160
|
+
args: {
|
|
161
|
+
label: "Upload Files",
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
export const Disabled: Story = {
|
|
166
|
+
args: {
|
|
167
|
+
label: "Upload Files",
|
|
168
|
+
disabled: true,
|
|
169
|
+
},
|
|
170
|
+
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
171
|
+
render: function FileUploadStory({
|
|
172
|
+
withLayer,
|
|
173
|
+
layer,
|
|
174
|
+
...args
|
|
175
|
+
}: FileUploadStoryArgs) {
|
|
176
|
+
/* eslint-enable @typescript-eslint/no-unused-vars */
|
|
177
|
+
const [files] = useState<RecursicaFileUploadItem[]>([
|
|
178
|
+
{ file: mockFile("document.pdf") },
|
|
179
|
+
]);
|
|
180
|
+
return <FileUpload {...args} files={files} />;
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
export const ErrorState: Story = {
|
|
185
|
+
args: {
|
|
186
|
+
label: "Upload Files",
|
|
187
|
+
error: "File upload failed. Please try again.",
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
function StarIcon() {
|
|
192
|
+
return (
|
|
193
|
+
<svg
|
|
194
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
195
|
+
viewBox="0 0 24 24"
|
|
196
|
+
fill="currentColor"
|
|
197
|
+
aria-hidden
|
|
198
|
+
>
|
|
199
|
+
<path d="M12 2l2.9 6.6 7.1.6-5.4 4.7 1.6 7-6.2-3.8-6.2 3.8 1.6-7L2 9.2l7.1-.6z" />
|
|
200
|
+
</svg>
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export const CustomIcon: Story = {
|
|
205
|
+
args: {
|
|
206
|
+
label: "Upload Files",
|
|
207
|
+
icon: <StarIcon />,
|
|
208
|
+
},
|
|
209
|
+
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
210
|
+
render: function FileUploadStory({
|
|
211
|
+
withLayer,
|
|
212
|
+
layer,
|
|
213
|
+
...args
|
|
214
|
+
}: FileUploadStoryArgs) {
|
|
215
|
+
/* eslint-enable @typescript-eslint/no-unused-vars */
|
|
216
|
+
const [files, setFiles] = useState<RecursicaFileUploadItem[]>([]);
|
|
217
|
+
return (
|
|
218
|
+
<FileUpload
|
|
219
|
+
{...args}
|
|
220
|
+
files={files}
|
|
221
|
+
onFilesAdded={(added) =>
|
|
222
|
+
setFiles((prev) => [...prev, ...added.map((file) => ({ file }))])
|
|
223
|
+
}
|
|
224
|
+
onFileRemove={(id) =>
|
|
225
|
+
setFiles((prev) =>
|
|
226
|
+
prev.filter((item) => (item.id ?? item.file.name) !== id),
|
|
227
|
+
)
|
|
228
|
+
}
|
|
229
|
+
/>
|
|
230
|
+
);
|
|
231
|
+
},
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
export const LongFilenames: Story = {
|
|
235
|
+
args: {
|
|
236
|
+
label: "Upload Files",
|
|
237
|
+
},
|
|
238
|
+
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
239
|
+
render: function FileUploadStory({
|
|
240
|
+
withLayer,
|
|
241
|
+
layer,
|
|
242
|
+
...args
|
|
243
|
+
}: FileUploadStoryArgs) {
|
|
244
|
+
/* eslint-enable @typescript-eslint/no-unused-vars */
|
|
245
|
+
const [files, setFiles] = useState<RecursicaFileUploadItem[]>([
|
|
246
|
+
{
|
|
247
|
+
file: mockFile("quarterly-financial-report-final-version-approved.pdf"),
|
|
248
|
+
},
|
|
249
|
+
{ file: mockFile("2026-08-team-offsite-photos-and-notes.zip") },
|
|
250
|
+
{ file: mockFile("resume.docx") },
|
|
251
|
+
]);
|
|
252
|
+
return (
|
|
253
|
+
<FileUpload
|
|
254
|
+
{...args}
|
|
255
|
+
files={files}
|
|
256
|
+
onFilesAdded={(added) =>
|
|
257
|
+
setFiles((prev) => [...prev, ...added.map((file) => ({ file }))])
|
|
258
|
+
}
|
|
259
|
+
onFileRemove={(id) =>
|
|
260
|
+
setFiles((prev) =>
|
|
261
|
+
prev.filter((item) => (item.id ?? item.file.name) !== id),
|
|
262
|
+
)
|
|
263
|
+
}
|
|
264
|
+
/>
|
|
265
|
+
);
|
|
266
|
+
},
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
export const ReadOnly: Story = {
|
|
270
|
+
args: {
|
|
271
|
+
label: "Upload Files",
|
|
272
|
+
assistiveText: "Submitted files cannot be changed",
|
|
273
|
+
readOnly: true,
|
|
274
|
+
},
|
|
275
|
+
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
276
|
+
render: function FileUploadStory({
|
|
277
|
+
withLayer,
|
|
278
|
+
layer,
|
|
279
|
+
...args
|
|
280
|
+
}: FileUploadStoryArgs) {
|
|
281
|
+
/* eslint-enable @typescript-eslint/no-unused-vars */
|
|
282
|
+
const [files] = useState<RecursicaFileUploadItem[]>([
|
|
283
|
+
{ file: mockFile("document.pdf") },
|
|
284
|
+
{ file: mockFile("image.png") },
|
|
285
|
+
]);
|
|
286
|
+
return <FileUpload {...args} files={files} />;
|
|
287
|
+
},
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
// Demonstrates that `accept` isn't just cosmetic on the browse-button dialog — files that don't
|
|
291
|
+
// match it are rejected the same way whether picked via Browse or dropped directly (drag-and-drop
|
|
292
|
+
// bypasses the native `accept` filtering entirely, so this only works because FileUpload
|
|
293
|
+
// re-validates it itself). Drag a non-.pdf/.png file onto the dropzone (or edit `accept` in the
|
|
294
|
+
// controls) to see the control switch into its error state with the default
|
|
295
|
+
// `invalidFileTypeMessage` ("File type not accepted") instead of accepting the file.
|
|
296
|
+
export const AcceptRestriction: Story = {
|
|
297
|
+
args: {
|
|
298
|
+
label: "Upload Files",
|
|
299
|
+
assistiveText: "Only .pdf and .png files are accepted",
|
|
300
|
+
accept: ".pdf,.png",
|
|
301
|
+
},
|
|
302
|
+
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
303
|
+
render: function FileUploadStory({
|
|
304
|
+
withLayer,
|
|
305
|
+
layer,
|
|
306
|
+
...args
|
|
307
|
+
}: FileUploadStoryArgs) {
|
|
308
|
+
/* eslint-enable @typescript-eslint/no-unused-vars */
|
|
309
|
+
const [files, setFiles] = useState<RecursicaFileUploadItem[]>([]);
|
|
310
|
+
return (
|
|
311
|
+
<FileUpload
|
|
312
|
+
{...args}
|
|
313
|
+
files={files}
|
|
314
|
+
onFilesAdded={(added) =>
|
|
315
|
+
setFiles((prev) => [...prev, ...added.map((file) => ({ file }))])
|
|
316
|
+
}
|
|
317
|
+
onFileRemove={(id) =>
|
|
318
|
+
setFiles((prev) =>
|
|
319
|
+
prev.filter((item) => (item.id ?? item.file.name) !== id),
|
|
320
|
+
)
|
|
321
|
+
}
|
|
322
|
+
/>
|
|
323
|
+
);
|
|
324
|
+
},
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
// Demonstrates `maxFiles`: already at the 2-file cap below, so dropping/picking another file is
|
|
328
|
+
// rejected the same way an `accept` mismatch is — the control switches into its error state with
|
|
329
|
+
// the default `maxFilesMessage` ("Maximum of 2 files allowed") instead of accepting the file.
|
|
330
|
+
export const MaxFilesRestriction: Story = {
|
|
331
|
+
args: {
|
|
332
|
+
label: "Upload Files",
|
|
333
|
+
assistiveText: "Up to 2 files allowed",
|
|
334
|
+
maxFiles: 2,
|
|
335
|
+
},
|
|
336
|
+
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
337
|
+
render: function FileUploadStory({
|
|
338
|
+
withLayer,
|
|
339
|
+
layer,
|
|
340
|
+
...args
|
|
341
|
+
}: FileUploadStoryArgs) {
|
|
342
|
+
/* eslint-enable @typescript-eslint/no-unused-vars */
|
|
343
|
+
const [files, setFiles] = useState<RecursicaFileUploadItem[]>([
|
|
344
|
+
{ file: mockFile("document.pdf") },
|
|
345
|
+
{ file: mockFile("image.png") },
|
|
346
|
+
]);
|
|
347
|
+
return (
|
|
348
|
+
<FileUpload
|
|
349
|
+
{...args}
|
|
350
|
+
files={files}
|
|
351
|
+
onFilesAdded={(added) =>
|
|
352
|
+
setFiles((prev) => [...prev, ...added.map((file) => ({ file }))])
|
|
353
|
+
}
|
|
354
|
+
onFileRemove={(id) =>
|
|
355
|
+
setFiles((prev) =>
|
|
356
|
+
prev.filter((item) => (item.id ?? item.file.name) !== id),
|
|
357
|
+
)
|
|
358
|
+
}
|
|
359
|
+
/>
|
|
360
|
+
);
|
|
361
|
+
},
|
|
362
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
// Placeholder for future FileUpload component tests. Add tests here if/when needed.
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import React, { forwardRef, useEffect, useRef, useState } from "react";
|
|
2
|
+
import {
|
|
3
|
+
filterStylingProps,
|
|
4
|
+
type RecursicaOverStyled,
|
|
5
|
+
} from "../../utils/filterStylingProps";
|
|
6
|
+
import { Button } from "../Button/Button";
|
|
7
|
+
import { Chip } from "../Chip/Chip";
|
|
8
|
+
import {
|
|
9
|
+
FormControlWrapper,
|
|
10
|
+
type RecursicaFormControlWrapperProps,
|
|
11
|
+
} from "../FormControlWrapper/FormControlWrapper";
|
|
12
|
+
import styles from "./FileUpload.module.css";
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
fileMatchesAccept,
|
|
16
|
+
type RecursicaFileUploadItem,
|
|
17
|
+
type RecursicaFileUploadProps as BaseRecursicaFileUploadProps,
|
|
18
|
+
} from "@recursica/adapter-common";
|
|
19
|
+
export type { RecursicaFileUploadItem };
|
|
20
|
+
|
|
21
|
+
export interface RecursicaFileUploadProps
|
|
22
|
+
extends Omit<
|
|
23
|
+
React.HTMLAttributes<HTMLDivElement>,
|
|
24
|
+
"children" | "onDrop" | "onChange"
|
|
25
|
+
>,
|
|
26
|
+
Omit<
|
|
27
|
+
RecursicaFormControlWrapperProps,
|
|
28
|
+
"controlMaxWidth" | "controlMinWidth"
|
|
29
|
+
>,
|
|
30
|
+
BaseRecursicaFileUploadProps {
|
|
31
|
+
/** Visually forces the mandatory asterisk. Accepted for API parity with other adapters; MUI's own `required` already renders the asterisk, so this has no separate effect here. */
|
|
32
|
+
withAsterisk?: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type FileUploadProps = RecursicaOverStyled<RecursicaFileUploadProps>;
|
|
36
|
+
|
|
37
|
+
function UploadIcon() {
|
|
38
|
+
return (
|
|
39
|
+
<svg
|
|
40
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
41
|
+
viewBox="0 0 24 24"
|
|
42
|
+
fill="none"
|
|
43
|
+
stroke="currentColor"
|
|
44
|
+
strokeWidth="2"
|
|
45
|
+
strokeLinecap="round"
|
|
46
|
+
strokeLinejoin="round"
|
|
47
|
+
aria-hidden
|
|
48
|
+
>
|
|
49
|
+
<path d="M12 3v12" />
|
|
50
|
+
<path d="M7 8l5-5 5 5" />
|
|
51
|
+
<path d="M4 21h16" />
|
|
52
|
+
</svg>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* A dropzone for uploading files, with a native browse-button fallback and a
|
|
58
|
+
* removable-chip list of the currently selected files.
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* <FileUpload
|
|
62
|
+
* label="Upload Files"
|
|
63
|
+
* assistiveText="Max file size 5MB"
|
|
64
|
+
* files={files}
|
|
65
|
+
* onFilesAdded={(added) => setFiles((f) => [...f, ...added.map((file) => ({ file }))])}
|
|
66
|
+
* onFileRemove={(id) => setFiles((f) => f.filter((item) => (item.id ?? item.file.name) !== id))}
|
|
67
|
+
* />
|
|
68
|
+
*/
|
|
69
|
+
export const FileUpload = forwardRef<HTMLDivElement, FileUploadProps>(
|
|
70
|
+
function FileUpload(props, ref) {
|
|
71
|
+
const {
|
|
72
|
+
overStyled = false,
|
|
73
|
+
formLayout = "stacked",
|
|
74
|
+
|
|
75
|
+
// Label & Wrapper Maps
|
|
76
|
+
labelSize,
|
|
77
|
+
labelAlignment,
|
|
78
|
+
labelOptionalText,
|
|
79
|
+
labelWithEditIcon,
|
|
80
|
+
labelActionArea,
|
|
81
|
+
onLabelEditClick,
|
|
82
|
+
|
|
83
|
+
label,
|
|
84
|
+
assistiveText,
|
|
85
|
+
assistiveWithIcon,
|
|
86
|
+
error,
|
|
87
|
+
required,
|
|
88
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
89
|
+
withAsterisk,
|
|
90
|
+
id,
|
|
91
|
+
className,
|
|
92
|
+
style,
|
|
93
|
+
disabled,
|
|
94
|
+
readOnly,
|
|
95
|
+
|
|
96
|
+
files,
|
|
97
|
+
onFilesAdded,
|
|
98
|
+
onFileRemove,
|
|
99
|
+
accept,
|
|
100
|
+
multiple = true,
|
|
101
|
+
maxSize,
|
|
102
|
+
maxFiles,
|
|
103
|
+
onFilesRejected,
|
|
104
|
+
invalidFileTypeMessage = "File type not accepted",
|
|
105
|
+
maxFilesMessage = `Maximum of ${maxFiles} files allowed`,
|
|
106
|
+
icon,
|
|
107
|
+
dropzoneLabel = "Drag and drop files here to upload",
|
|
108
|
+
browseButtonLabel = "Browse files",
|
|
109
|
+
removeFileLabel = "Remove",
|
|
110
|
+
...rest
|
|
111
|
+
} = props;
|
|
112
|
+
|
|
113
|
+
const sanitizedProps = filterStylingProps(rest, overStyled);
|
|
114
|
+
const restRecord = sanitizedProps as Record<string, unknown>;
|
|
115
|
+
|
|
116
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
117
|
+
|
|
118
|
+
// Whether the most recent drop/pick attempt included a file that failed the `accept` check —
|
|
119
|
+
// surfaced as the control's error state (see `effectiveError` below) rather than left for the
|
|
120
|
+
// integrator to wire up themselves.
|
|
121
|
+
const [invalidTypeRejected, setInvalidTypeRejected] = useState(false);
|
|
122
|
+
// Whether the most recent drop/pick attempt included a file that would have pushed the total
|
|
123
|
+
// past `maxFiles` — surfaced the same way as `invalidTypeRejected` (see `effectiveError`).
|
|
124
|
+
const [tooManyFilesRejected, setTooManyFilesRejected] = useState(false);
|
|
125
|
+
|
|
126
|
+
const handleFiles = (incoming: FileList | File[]) => {
|
|
127
|
+
if (disabled) return;
|
|
128
|
+
const list = Array.from(incoming);
|
|
129
|
+
if (list.length === 0) return;
|
|
130
|
+
|
|
131
|
+
const currentCount = files?.length ?? 0;
|
|
132
|
+
const accepted: File[] = [];
|
|
133
|
+
const rejected: File[] = [];
|
|
134
|
+
let hasInvalidType = false;
|
|
135
|
+
let hasTooMany = false;
|
|
136
|
+
for (const file of list) {
|
|
137
|
+
const isInvalidType = !fileMatchesAccept(file, accept);
|
|
138
|
+
if (isInvalidType) hasInvalidType = true;
|
|
139
|
+
const isTooLarge = maxSize !== undefined && file.size > maxSize;
|
|
140
|
+
const wouldExceedMax =
|
|
141
|
+
maxFiles !== undefined && currentCount + accepted.length >= maxFiles;
|
|
142
|
+
if (wouldExceedMax) hasTooMany = true;
|
|
143
|
+
const isRejected = isInvalidType || isTooLarge || wouldExceedMax;
|
|
144
|
+
(isRejected ? rejected : accepted).push(file);
|
|
145
|
+
}
|
|
146
|
+
setInvalidTypeRejected(hasInvalidType);
|
|
147
|
+
setTooManyFilesRejected(hasTooMany);
|
|
148
|
+
if (accepted.length > 0) onFilesAdded?.(accepted);
|
|
149
|
+
if (rejected.length > 0) onFilesRejected?.(rejected);
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
// Counts nested dragenter/dragleave pairs (they fire for every child element the pointer
|
|
153
|
+
// crosses, not just the dropzone itself) so the drag-over visual state only clears once the
|
|
154
|
+
// pointer has actually left the dropzone, not just moved between its children.
|
|
155
|
+
const dragCounterRef = useRef(0);
|
|
156
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
157
|
+
|
|
158
|
+
const handleDragEnter = (event: React.DragEvent<HTMLDivElement>) => {
|
|
159
|
+
event.preventDefault();
|
|
160
|
+
dragCounterRef.current += 1;
|
|
161
|
+
setIsDragging(true);
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const handleDragLeave = (event: React.DragEvent<HTMLDivElement>) => {
|
|
165
|
+
event.preventDefault();
|
|
166
|
+
dragCounterRef.current -= 1;
|
|
167
|
+
if (dragCounterRef.current <= 0) {
|
|
168
|
+
dragCounterRef.current = 0;
|
|
169
|
+
setIsDragging(false);
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const handleDragOver = (event: React.DragEvent<HTMLDivElement>) => {
|
|
174
|
+
// Required so the browser treats this element as a valid drop target.
|
|
175
|
+
event.preventDefault();
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const handleDrop = (event: React.DragEvent<HTMLDivElement>) => {
|
|
179
|
+
event.preventDefault();
|
|
180
|
+
dragCounterRef.current = 0;
|
|
181
|
+
setIsDragging(false);
|
|
182
|
+
handleFiles(event.dataTransfer.files);
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const openFilePicker = () => {
|
|
186
|
+
if (disabled) return;
|
|
187
|
+
inputRef.current?.click();
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
191
|
+
if (event.target.files) handleFiles(event.target.files);
|
|
192
|
+
// Reset so picking the same file again still fires a change event.
|
|
193
|
+
event.target.value = "";
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
// Roving tabindex across the file chip list: only the "active" chip's delete icon is a tab
|
|
197
|
+
// stop (Tab lands on the first chip), and Left/Right/Up/Down move it — see
|
|
198
|
+
// FILEUPLOAD_IMPLEMENTATION_NOTES.md.
|
|
199
|
+
const [activeChipIndex, setActiveChipIndex] = useState(0);
|
|
200
|
+
const deleteIconRefs = useRef<Array<HTMLSpanElement | null>>([]);
|
|
201
|
+
const prevFileCountRef = useRef(files?.length ?? 0);
|
|
202
|
+
|
|
203
|
+
useEffect(() => {
|
|
204
|
+
const count = files?.length ?? 0;
|
|
205
|
+
// A chip was removed (via keyboard or otherwise) — keep focus in the list, clamped to the
|
|
206
|
+
// new length, rather than letting it fall back to the document body.
|
|
207
|
+
if (count > 0 && count < prevFileCountRef.current) {
|
|
208
|
+
const nextIndex = Math.min(activeChipIndex, count - 1);
|
|
209
|
+
setActiveChipIndex(nextIndex);
|
|
210
|
+
deleteIconRefs.current[nextIndex]?.focus();
|
|
211
|
+
}
|
|
212
|
+
prevFileCountRef.current = count;
|
|
213
|
+
// Only react to the file list itself shrinking/growing, not to activeChipIndex changes.
|
|
214
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
215
|
+
}, [files]);
|
|
216
|
+
|
|
217
|
+
const handleFileListKeyDown = (
|
|
218
|
+
event: React.KeyboardEvent<HTMLDivElement>,
|
|
219
|
+
) => {
|
|
220
|
+
const count = files?.length ?? 0;
|
|
221
|
+
if (count === 0) return;
|
|
222
|
+
let nextIndex: number | undefined;
|
|
223
|
+
if (event.key === "ArrowRight" || event.key === "ArrowDown") {
|
|
224
|
+
nextIndex = (activeChipIndex + 1) % count;
|
|
225
|
+
} else if (event.key === "ArrowLeft" || event.key === "ArrowUp") {
|
|
226
|
+
nextIndex = (activeChipIndex - 1 + count) % count;
|
|
227
|
+
}
|
|
228
|
+
if (nextIndex === undefined) return;
|
|
229
|
+
event.preventDefault();
|
|
230
|
+
setActiveChipIndex(nextIndex);
|
|
231
|
+
deleteIconRefs.current[nextIndex]?.focus();
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
// The built-in `accept`-mismatch message is only shown when the integrator hasn't supplied
|
|
235
|
+
// their own `error` — an explicit error always wins.
|
|
236
|
+
const effectiveError =
|
|
237
|
+
error ??
|
|
238
|
+
(invalidTypeRejected
|
|
239
|
+
? invalidFileTypeMessage
|
|
240
|
+
: tooManyFilesRejected
|
|
241
|
+
? maxFilesMessage
|
|
242
|
+
: undefined);
|
|
243
|
+
|
|
244
|
+
const wrapperClass = className
|
|
245
|
+
? `${styles.layoutOverride} ${className}`
|
|
246
|
+
: styles.layoutOverride;
|
|
247
|
+
|
|
248
|
+
return (
|
|
249
|
+
<FormControlWrapper
|
|
250
|
+
overStyled={overStyled}
|
|
251
|
+
className={wrapperClass}
|
|
252
|
+
style={style}
|
|
253
|
+
formLayout={formLayout}
|
|
254
|
+
labelSize={labelSize}
|
|
255
|
+
labelAlignment={labelAlignment}
|
|
256
|
+
labelOptionalText={labelOptionalText}
|
|
257
|
+
labelWithEditIcon={labelWithEditIcon}
|
|
258
|
+
labelActionArea={labelActionArea}
|
|
259
|
+
onLabelEditClick={onLabelEditClick}
|
|
260
|
+
label={label}
|
|
261
|
+
assistiveText={assistiveText}
|
|
262
|
+
assistiveWithIcon={assistiveWithIcon}
|
|
263
|
+
error={effectiveError}
|
|
264
|
+
required={required}
|
|
265
|
+
disabled={disabled}
|
|
266
|
+
id={id}
|
|
267
|
+
>
|
|
268
|
+
<div
|
|
269
|
+
ref={ref}
|
|
270
|
+
{...restRecord}
|
|
271
|
+
className={styles.root}
|
|
272
|
+
data-disabled={disabled ? "true" : undefined}
|
|
273
|
+
data-error={effectiveError ? "true" : undefined}
|
|
274
|
+
>
|
|
275
|
+
{!readOnly && (
|
|
276
|
+
<div
|
|
277
|
+
className={styles.dropzone}
|
|
278
|
+
data-dragging={isDragging ? "true" : undefined}
|
|
279
|
+
onDragEnter={disabled ? undefined : handleDragEnter}
|
|
280
|
+
onDragLeave={disabled ? undefined : handleDragLeave}
|
|
281
|
+
onDragOver={disabled ? undefined : handleDragOver}
|
|
282
|
+
onDrop={disabled ? undefined : handleDrop}
|
|
283
|
+
>
|
|
284
|
+
<span className={styles.uploadIcon}>
|
|
285
|
+
{icon ?? <UploadIcon />}
|
|
286
|
+
</span>
|
|
287
|
+
<span className={styles.dropzoneText}>{dropzoneLabel}</span>
|
|
288
|
+
<Button
|
|
289
|
+
variant="outline"
|
|
290
|
+
disabled={disabled}
|
|
291
|
+
onClick={openFilePicker}
|
|
292
|
+
>
|
|
293
|
+
{browseButtonLabel}
|
|
294
|
+
</Button>
|
|
295
|
+
<input
|
|
296
|
+
ref={inputRef}
|
|
297
|
+
type="file"
|
|
298
|
+
hidden
|
|
299
|
+
accept={accept}
|
|
300
|
+
multiple={multiple}
|
|
301
|
+
disabled={disabled}
|
|
302
|
+
onChange={handleInputChange}
|
|
303
|
+
/>
|
|
304
|
+
</div>
|
|
305
|
+
)}
|
|
306
|
+
|
|
307
|
+
{files &&
|
|
308
|
+
files.length > 0 &&
|
|
309
|
+
(readOnly ? (
|
|
310
|
+
<div className={styles.fileList}>
|
|
311
|
+
{files.map((item: RecursicaFileUploadItem) => {
|
|
312
|
+
const itemId = item.id ?? item.file.name;
|
|
313
|
+
return (
|
|
314
|
+
<Chip key={itemId} tabIndex={-1}>
|
|
315
|
+
{item.file.name}
|
|
316
|
+
</Chip>
|
|
317
|
+
);
|
|
318
|
+
})}
|
|
319
|
+
</div>
|
|
320
|
+
) : (
|
|
321
|
+
<div
|
|
322
|
+
className={styles.fileList}
|
|
323
|
+
onKeyDown={handleFileListKeyDown}
|
|
324
|
+
>
|
|
325
|
+
{files.map((item: RecursicaFileUploadItem, index) => {
|
|
326
|
+
const itemId = item.id ?? item.file.name;
|
|
327
|
+
return (
|
|
328
|
+
<Chip
|
|
329
|
+
key={itemId}
|
|
330
|
+
tabIndex={-1}
|
|
331
|
+
deleteLabel={removeFileLabel}
|
|
332
|
+
deleteTabIndex={index === activeChipIndex ? 0 : -1}
|
|
333
|
+
deleteIconRef={(el) => {
|
|
334
|
+
deleteIconRefs.current[index] = el;
|
|
335
|
+
}}
|
|
336
|
+
onDelete={
|
|
337
|
+
disabled ? undefined : () => onFileRemove?.(itemId)
|
|
338
|
+
}
|
|
339
|
+
>
|
|
340
|
+
{item.file.name}
|
|
341
|
+
</Chip>
|
|
342
|
+
);
|
|
343
|
+
})}
|
|
344
|
+
</div>
|
|
345
|
+
))}
|
|
346
|
+
</div>
|
|
347
|
+
</FormControlWrapper>
|
|
348
|
+
);
|
|
349
|
+
},
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
FileUpload.displayName = "FileUpload";
|