|
| 1 | +{/* Copyright 2023 Adobe. All rights reserved. |
| 2 | +This file is licensed to you under the Apache License, Version 2.0 (the "License"); |
| 3 | +you may not use this file except in compliance with the License. You may obtain a copy |
| 4 | +of the License at http://www.apache.org/licenses/LICENSE-2.0 |
| 5 | +Unless required by applicable law or agreed to in writing, software distributed under |
| 6 | +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS |
| 7 | +OF ANY KIND, either express or implied. See the License for the specific language |
| 8 | +governing permissions and limitations under the License. */} |
| 9 | + |
| 10 | +import {Layout} from '@react-spectrum/docs'; |
| 11 | +export default Layout; |
| 12 | + |
| 13 | +import docs from 'docs:@react-spectrum/dropzone'; |
| 14 | +import {HeaderInfo, PropTable, PageDescription} from '@react-spectrum/docs'; |
| 15 | +import packageData from '@react-spectrum/dropzone/package.json'; |
| 16 | +import ChevronRight from '@spectrum-icons/workflow/ChevronRight'; |
| 17 | +import styles from '@react-spectrum/docs/src/docs.css'; |
| 18 | + |
| 19 | +```jsx import |
| 20 | +import {DropZone} from '@react-spectrum/dropzone'; |
| 21 | +import {Heading} from '@react-spectrum/text'; |
| 22 | +import {Content} from '@react-spectrum/view'; |
| 23 | +import {IllustratedMessage} from '@react-spectrum/illustratedmessage'; |
| 24 | +import {Text} from 'react-aria-components'; |
| 25 | +import {FileTrigger} from 'react-aria-components'; |
| 26 | +import {Button} from '@react-spectrum/button'; |
| 27 | +``` |
| 28 | + |
| 29 | +--- |
| 30 | +category: Drag and drop |
| 31 | +--- |
| 32 | + |
| 33 | +# DropZone |
| 34 | + |
| 35 | +<PageDescription>{docs.exports.DropZone.description}</PageDescription> |
| 36 | + |
| 37 | +<HeaderInfo |
| 38 | + packageData={packageData} |
| 39 | + componentNames={['DropZone']} |
| 40 | +/> |
| 41 | + |
| 42 | +## Example |
| 43 | + |
| 44 | +```tsx example |
| 45 | +import Upload from '@spectrum-icons/illustrations/Upload'; |
| 46 | + |
| 47 | +<DropZone |
| 48 | + maxWidth="size-3000"> |
| 49 | + <IllustratedMessage> |
| 50 | + <Upload /> |
| 51 | + <Heading> |
| 52 | + <Text slot="label"> |
| 53 | + Drag and drop your file |
| 54 | + </Text> |
| 55 | + </Heading> |
| 56 | + </IllustratedMessage> |
| 57 | +</DropZone> |
| 58 | +``` |
| 59 | + |
| 60 | + |
| 61 | +## Content |
| 62 | + |
| 63 | +A drop zone accepts an [IllustratedMessage](IllustratedMessage.html) as a child which is comprised of three areas: an illustration, a title, and a body. Each of these sections can be populated by providing the following components to the IllustratedMessage as children: a SVG, a [Heading](Heading.html) (title), and a [Content](Content.html) (body). A [FileTrigger](../react-aria/FileTrigger.html) is commonly paired with a DropZone to allow a user to choose files from their device. |
| 64 | + |
| 65 | +```tsx example |
| 66 | +<DropZone |
| 67 | + maxWidth="size-3000"> |
| 68 | + <IllustratedMessage> |
| 69 | + <Upload /> |
| 70 | + <Heading> |
| 71 | + <Text slot="label"> |
| 72 | + Drag and drop here |
| 73 | + </Text> |
| 74 | + </Heading> |
| 75 | + <Content> |
| 76 | + <FileTrigger> |
| 77 | + <Button variant="primary">Browse</Button> |
| 78 | + </FileTrigger> |
| 79 | + </Content> |
| 80 | + </IllustratedMessage> |
| 81 | +</DropZone> |
| 82 | +``` |
| 83 | + |
| 84 | +### Accessibility |
| 85 | + |
| 86 | +A visual label should be provided to `DropZone` using a `Text` element with a `label` slot. If it is not provided, then an `aria-label` or `aria-labelledby` prop must be passed to identify the visually hidden button to assistive technology. |
| 87 | + |
| 88 | +### Internationalization |
| 89 | + |
| 90 | +In order to internationalize a drop zone, a localized string should be passed to the `Text` element with a `label` slot or to the `aria-label` prop, in addition to the `replaceMessage` prop. |
| 91 | + |
| 92 | +## Events |
| 93 | + |
| 94 | +`DropZone` supports drop operations via mouse, keyboard, and touch. You can handle all of these via the `onDrop` prop. In addition, the `onDropEnter`, `onDropMove`, and `onDropExit` events are fired as the user enter and exists the dropzone during a drag operation. |
| 95 | + |
| 96 | +The following example uses an `onDrop` handler to update the filled status stored in React state. |
| 97 | + |
| 98 | +```tsx example |
| 99 | +import File from '@spectrum-icons/illustrations/File'; |
| 100 | + |
| 101 | +function Example() { |
| 102 | + let [isFilled, setIsFilled] = React.useState(false); |
| 103 | + let [filledSrc, setFilledSrc] = React.useState(null); |
| 104 | + |
| 105 | + return ( |
| 106 | + <> |
| 107 | + <Draggable /> |
| 108 | + <DropZone |
| 109 | + maxWidth="size-3000" |
| 110 | + isFilled={isFilled} |
| 111 | + onDrop={async (e) => { |
| 112 | + e.items.find(async (item) => { |
| 113 | + if (item.kind === 'file') { |
| 114 | + setFilledSrc(item.name); |
| 115 | + setIsFilled(true); |
| 116 | + |
| 117 | + } else if (item.kind === 'text' && item.types.has('text/plain')) { |
| 118 | + setFilledSrc(await item.getText('text/plain')); |
| 119 | + setIsFilled(true); |
| 120 | + } |
| 121 | + }); |
| 122 | + }}> |
| 123 | + <IllustratedMessage> |
| 124 | + <Upload /> |
| 125 | + <Heading> |
| 126 | + <Text slot="label"> |
| 127 | + Drag and drop here |
| 128 | + </Text> |
| 129 | + </Heading> |
| 130 | + </IllustratedMessage> |
| 131 | + </DropZone> |
| 132 | + {isFilled && |
| 133 | + <div className="files"> |
| 134 | + <File /> |
| 135 | + {filledSrc} |
| 136 | + </div>} |
| 137 | + </> |
| 138 | + ); |
| 139 | +} |
| 140 | +``` |
| 141 | + |
| 142 | +The `Draggable` component used above is defined below. See [useDrag](../react-aria/useDrag.html) for more details and documentation. |
| 143 | + |
| 144 | +<details> |
| 145 | + <summary style={{fontWeight: 'bold'}}><ChevronRight size="S" /> Show code</summary> |
| 146 | + |
| 147 | +```tsx example render=false export=true |
| 148 | +import {useDrag} from '@react-aria/dnd'; |
| 149 | + |
| 150 | +function Draggable() { |
| 151 | + let {dragProps, isDragging} = useDrag({ |
| 152 | + getItems() { |
| 153 | + return [{ |
| 154 | + 'text/plain': 'hello world', |
| 155 | + 'my-app-custom-type': JSON.stringify({message: 'hello world'}) |
| 156 | + }]; |
| 157 | + } |
| 158 | + }); |
| 159 | + |
| 160 | + return ( |
| 161 | + <div {...dragProps} role="button" tabIndex={0} className={`draggable ${isDragging ? 'dragging' : ''}`}> |
| 162 | + Drag me |
| 163 | + </div> |
| 164 | + ); |
| 165 | +} |
| 166 | +``` |
| 167 | +</details> |
| 168 | + |
| 169 | +<details> |
| 170 | + <summary style={{fontWeight: 'bold'}}><ChevronRight size="S" /> Show CSS</summary> |
| 171 | + |
| 172 | +```css |
| 173 | +.draggable { |
| 174 | + display: inline-block; |
| 175 | + vertical-align: top; |
| 176 | + border: 1px solid gray; |
| 177 | + padding: 10px; |
| 178 | + margin-right: 20px; |
| 179 | + margin-bottom: 20px; |
| 180 | + border-radius: 4px; |
| 181 | + height: fit-content; |
| 182 | +} |
| 183 | + |
| 184 | +.draggable.dragging { |
| 185 | + opacity: 0.5; |
| 186 | +} |
| 187 | + |
| 188 | +.files{ |
| 189 | + margin-top: 20px; |
| 190 | +} |
| 191 | +``` |
| 192 | + |
| 193 | +</details> |
| 194 | + |
| 195 | + |
| 196 | +## Props |
| 197 | + |
| 198 | +<PropTable component={docs.exports.DropZone} links={docs.links} /> |
| 199 | + |
| 200 | +## Visual options |
| 201 | + |
| 202 | +### Filled state |
| 203 | + |
| 204 | +The user is responsible for both managing the filled state of a drop zone and handling the associated styling. To set the drop zone to a filled state, the user must pass the `isFilled` prop. |
| 205 | + |
| 206 | +The example below demonstrates one way of styling the filled state. |
| 207 | + |
| 208 | +```tsx example |
| 209 | +function Example() { |
| 210 | + let [filledSrc, setFilledSrc] = React.useState(null); |
| 211 | + let [isFilled, setIsFilled] = React.useState(false); |
| 212 | + |
| 213 | + return ( |
| 214 | + <> |
| 215 | + <DraggableImage /> |
| 216 | + <DropZone |
| 217 | + isFilled={isFilled} |
| 218 | + maxWidth="size-3000" |
| 219 | + height="size-2400" |
| 220 | + getDropOperation={(types) => (types.has('image/png') || types.has('image/jpeg')) ? 'copy' : 'cancel'} |
| 221 | + onDrop={async (e) => { |
| 222 | + e.items.find(async (item) => { |
| 223 | + if (item.kind === 'file') { |
| 224 | + if (item.type === 'image/jpeg' || item.type === 'image/png') { |
| 225 | + setFilledSrc(URL.createObjectURL(await item.getFile())); |
| 226 | + setIsFilled(true); |
| 227 | + } |
| 228 | + } else if (item.kind === 'text') { |
| 229 | + setFilledSrc(await item.getText('image/jpeg')); |
| 230 | + setIsFilled(true); |
| 231 | + } |
| 232 | + }); |
| 233 | + }}> |
| 234 | + <IllustratedMessage> |
| 235 | + <Upload /> |
| 236 | + <Heading> |
| 237 | + <Text slot="label"> |
| 238 | + Drag and drop photos |
| 239 | + </Text> |
| 240 | + </Heading> |
| 241 | + </IllustratedMessage> |
| 242 | + {isFilled && <img className={'images'} alt="" src={filledSrc} />} |
| 243 | + </DropZone> |
| 244 | + </> |
| 245 | + ); |
| 246 | +} |
| 247 | +``` |
| 248 | + |
| 249 | +<details> |
| 250 | + <summary style={{fontWeight: 'bold'}}><ChevronRight size="S" /> Show CSS</summary> |
| 251 | + |
| 252 | +```css |
| 253 | +.images { |
| 254 | + position: absolute; |
| 255 | + top: 0px; |
| 256 | + left: 0px; |
| 257 | + width: 100%; |
| 258 | + height: 100%; |
| 259 | + object-fit: cover; |
| 260 | + border-radius: var(--spectrum-alias-border-radius-small); |
| 261 | +} |
| 262 | +``` |
| 263 | +</details> |
| 264 | + |
| 265 | +The `DraggableImage` component used above is defined below. See [useDrag](../react-aria/useDrag.html) for more details and documentation. |
| 266 | + |
| 267 | +<details> |
| 268 | + <summary style={{fontWeight: 'bold'}}><ChevronRight size="S" /> Show code</summary> |
| 269 | + |
| 270 | +```tsx example render=false export=true |
| 271 | +function DraggableImage() { |
| 272 | + let {dragProps, isDragging} = useDrag({ |
| 273 | + getItems() { |
| 274 | + return [ |
| 275 | + { |
| 276 | + 'image/jpeg': 'https://i.imgur.com/Z7AzH2c.jpg' |
| 277 | + } |
| 278 | + ]; |
| 279 | + } |
| 280 | + }); |
| 281 | + |
| 282 | + return ( |
| 283 | + <div |
| 284 | + {...dragProps} |
| 285 | + role="button" |
| 286 | + tabIndex={0} |
| 287 | + className={`draggable ${isDragging ? 'dragging' : ''}`} > |
| 288 | + <img |
| 289 | + width="150px" |
| 290 | + height="100px" |
| 291 | + alt="Traditional Roof" |
| 292 | + src="https://i.imgur.com/Z7AzH2c.jpg"/> |
| 293 | + </div> |
| 294 | + ); |
| 295 | +} |
| 296 | +``` |
| 297 | +</details> |
| 298 | + |
| 299 | +### Replace message |
| 300 | + |
| 301 | +When a drop zone is in a filled state and has an object dragged over it, a message will appear in front of the drop zone. By default, this message will say "Drop file to replace". However, users can choose to customize this message through the `replaceMessage` prop. This message should describe the interaction that will occur when the object is dropped. It should also be internationalized if needed. |
| 302 | + |
| 303 | + |
| 304 | +```tsx example |
| 305 | +function Example() { |
| 306 | + let [isFilled, setIsFilled] = React.useState(false); |
| 307 | + |
| 308 | + return ( |
| 309 | + <> |
| 310 | + <Draggable /> |
| 311 | + <DropZone |
| 312 | + isFilled={isFilled} |
| 313 | + maxWidth="size-3000" |
| 314 | + replaceMessage="This is a custom message" |
| 315 | + onDrop={() => setIsFilled(true)}> |
| 316 | + <IllustratedMessage> |
| 317 | + <Upload /> |
| 318 | + <Heading> |
| 319 | + <Text slot="label"> |
| 320 | + Drag and drop here |
| 321 | + </Text> |
| 322 | + </Heading> |
| 323 | + </IllustratedMessage> |
| 324 | + </DropZone> |
| 325 | + </> |
| 326 | + ); |
| 327 | +} |
| 328 | +``` |
| 329 | + |
| 330 | +### Visual feedback |
| 331 | + |
| 332 | +A drop zone displays visual feedback to the user when a drag hovers over the drop target by passing the `getDropOperation` function. If a drop target only supports data of specific types (e.g. images, videos, text, etc.), then it should implement the `getDropOperation` prop and return `cancel` for types that aren't supported. This will prevent visual feedback indicating that the drop target accepts the dragged data when this is not true. [Read more about getDropOperation.](../react-aria/useDrop.html#getdropoperation) |
| 333 | + |
| 334 | +```tsx example |
| 335 | + |
| 336 | +function Example() { |
| 337 | + let [isFilled, setIsFilled] = React.useState(false); |
| 338 | + |
| 339 | + return ( |
| 340 | + <DropZone |
| 341 | + maxWidth="size-3000" |
| 342 | + isFilled={isFilled} |
| 343 | + getDropOperation={(types) => types.has('image/png') ? 'copy' : 'cancel'} |
| 344 | + onDrop={() => setIsFilled(true)}> |
| 345 | + <IllustratedMessage> |
| 346 | + <Upload /> |
| 347 | + <Heading> |
| 348 | + <Text slot="label"> |
| 349 | + Drag and drop here |
| 350 | + </Text> |
| 351 | + </Heading> |
| 352 | + <Content> |
| 353 | + <FileTrigger> |
| 354 | + <Button variant="primary">Browse</Button> |
| 355 | + </FileTrigger> |
| 356 | + </Content> |
| 357 | + </IllustratedMessage> |
| 358 | + </DropZone> |
| 359 | + ); |
| 360 | +} |
| 361 | +``` |
0 commit comments