diff --git a/packages/react-native/Libraries/Components/Pressable/Pressable.js b/packages/react-native/Libraries/Components/Pressable/Pressable.js index 9e13dcef486fe0..4f2d7f7e36b083 100644 --- a/packages/react-native/Libraries/Components/Pressable/Pressable.js +++ b/packages/react-native/Libraries/Components/Pressable/Pressable.js @@ -12,6 +12,7 @@ import type { BlurEvent, // [macOS FocusEvent, + DragEvent, HandledKeyEvent, KeyEvent, GestureResponderEvent, @@ -198,21 +199,21 @@ type PressableBaseProps = $ReadOnly<{ * * @platform macos */ - onDragEnter?: (event: MouseEvent) => void, + onDragEnter?: (event: DragEvent) => void, /** * Fired when a file is dragged out of the Pressable via the mouse. * * @platform macos */ - onDragLeave?: (event: MouseEvent) => void, + onDragLeave?: (event: DragEvent) => void, /** * Fired when a file is dropped on the Pressable via the mouse. * * @platform macos */ - onDrop?: (event: MouseEvent) => void, + onDrop?: (event: DragEvent) => void, /** * The types of dragged files that the Pressable will accept. diff --git a/packages/react-native/Libraries/Components/Touchable/TouchableWithoutFeedback.js b/packages/react-native/Libraries/Components/Touchable/TouchableWithoutFeedback.js index 71f9ebc257b0d9..bb7c577f2dfc40 100755 --- a/packages/react-native/Libraries/Components/Touchable/TouchableWithoutFeedback.js +++ b/packages/react-native/Libraries/Components/Touchable/TouchableWithoutFeedback.js @@ -13,8 +13,8 @@ import type {EdgeInsetsOrSizeProp} from '../../StyleSheet/EdgeInsetsPropType'; import type { BlurEvent, FocusEvent, - // [macOS] - MouseEvent, + MouseEvent, // [macOS] + DragEvent, // [macOS] GestureResponderEvent, LayoutChangeEvent, } from '../../Types/CoreEventTypes'; @@ -36,9 +36,9 @@ export type TouchableWithoutFeedbackPropsIOS = { tooltip?: ?string, onMouseEnter?: (event: MouseEvent) => void, onMouseLeave?: (event: MouseEvent) => void, - onDragEnter?: (event: MouseEvent) => void, - onDragLeave?: (event: MouseEvent) => void, - onDrop?: (event: MouseEvent) => void, + onDragEnter?: (event: DragEvent) => void, + onDragLeave?: (event: DragEvent) => void, + onDrop?: (event: DragEvent) => void, draggedTypes?: ?DraggedTypesType, // macOS] }; diff --git a/packages/react-native/Libraries/Components/View/DraggedType.js b/packages/react-native/Libraries/Components/View/DraggedType.js index dc02cf4cb6c02d..2e4ff89ca045d0 100644 --- a/packages/react-native/Libraries/Components/View/DraggedType.js +++ b/packages/react-native/Libraries/Components/View/DraggedType.js @@ -12,10 +12,10 @@ 'use strict'; -export type DraggedType = 'fileUrl'; +export type DraggedType = 'fileUrl' | 'image' | 'string'; export type DraggedTypesType = DraggedType | $ReadOnlyArray; module.exports = { - DraggedTypes: ['fileUrl'], + DraggedTypes: ['fileUrl', 'image', 'string'], }; diff --git a/packages/react-native/Libraries/Components/View/ViewPropTypes.d.ts b/packages/react-native/Libraries/Components/View/ViewPropTypes.d.ts index 22c971ded8bfb5..76e8f1bf0cc37e 100644 --- a/packages/react-native/Libraries/Components/View/ViewPropTypes.d.ts +++ b/packages/react-native/Libraries/Components/View/ViewPropTypes.d.ts @@ -13,6 +13,7 @@ import {GestureResponderHandlers} from '../../../types/public/ReactNativeRendere import {StyleProp} from '../../StyleSheet/StyleSheet'; import {ViewStyle} from '../../StyleSheet/StyleSheetTypes'; import { + DragEvent, HandledKeyEvent, KeyEvent, LayoutChangeEvent, @@ -108,7 +109,7 @@ export interface ViewPropsAndroid { tabIndex?: 0 | -1 | undefined; } -export type DraggedType = 'fileUrl'; +export type DraggedType = 'fileUrl' | 'image' | 'string'; export type DraggedTypesType = DraggedType | DraggedType[]; export interface ViewPropsMacOS { @@ -118,9 +119,9 @@ export interface ViewPropsMacOS { enableFocusRing?: boolean | undefined; onMouseEnter?: ((event: MouseEvent) => void) | undefined; onMouseLeave?: ((event: MouseEvent) => void) | undefined; - onDragEnter?: ((event: MouseEvent) => void) | undefined; - onDragLeave?: ((event: MouseEvent) => void) | undefined; - onDrop?: ((event: MouseEvent) => void) | undefined; + onDragEnter?: ((event: DragEvent) => void) | undefined; + onDragLeave?: ((event: DragEvent) => void) | undefined; + onDrop?: ((event: DragEvent) => void) | undefined; onKeyDown?: ((event: KeyEvent) => void) | undefined; onKeyUp?: ((event: KeyEvent) => void) | undefined; keyDownEvents?: HandledKeyEvent[] | undefined; diff --git a/packages/react-native/Libraries/Components/View/ViewPropTypes.js b/packages/react-native/Libraries/Components/View/ViewPropTypes.js index 5fd3186776963c..37532c8cf330fe 100644 --- a/packages/react-native/Libraries/Components/View/ViewPropTypes.js +++ b/packages/react-native/Libraries/Components/View/ViewPropTypes.js @@ -16,6 +16,7 @@ import type { BlurEvent, FocusEvent, // [macOS] + DragEvent, HandledKeyEvent, KeyEvent, LayoutChangeEvent, @@ -410,21 +411,21 @@ type MacOSViewProps = $ReadOnly<{| * * @platform macos */ - onDragEnter?: (event: MouseEvent) => void, + onDragEnter?: (event: DragEvent) => void, /** * Fired when a file is dragged out of the view via the mouse. * * @platform macos */ - onDragLeave?: (event: MouseEvent) => void, + onDragLeave?: (event: DragEvent) => void, /** * Fired when an element is dropped on a valid drop target * * @platform macos */ - onDrop?: (event: MouseEvent) => void, + onDrop?: (event: DragEvent) => void, /** * Specifies the Tooltip for the view diff --git a/packages/react-native/Libraries/Types/CoreEventTypes.d.ts b/packages/react-native/Libraries/Types/CoreEventTypes.d.ts index 5e672c61baed8e..822ae7cccba033 100644 --- a/packages/react-native/Libraries/Types/CoreEventTypes.d.ts +++ b/packages/react-native/Libraries/Types/CoreEventTypes.d.ts @@ -305,4 +305,26 @@ export interface NativeBlurEvent extends TargetedEvent {} export interface FocusEvent extends NativeSyntheticEvent {} export interface BlueEvent extends NativeSyntheticEvent {} + +// Drag and Drop types +export interface DataTransferItem { + name: string; + kind: string; + type: string; + uri: string; + size?: number | undefined; + width?: number | undefined; + height?: number | undefined; +} + +export interface DataTransfer { + files: ReadonlyArray; + types: ReadonlyArray; +} + +export interface DragEvent extends MouseEvent { + nativeEvent: NativeMouseEvent & { + dataTransfer?: DataTransfer | undefined; + }; +} // macOS] diff --git a/packages/react-native/Libraries/Types/CoreEventTypes.js b/packages/react-native/Libraries/Types/CoreEventTypes.js index b568c59f6bcaab..54f837e31fc0e0 100644 --- a/packages/react-native/Libraries/Types/CoreEventTypes.js +++ b/packages/react-native/Libraries/Types/CoreEventTypes.js @@ -221,13 +221,10 @@ export interface NativePointerEvent extends NativeMouseEvent { export type PointerEvent = NativeSyntheticEvent; export type NativeTouchEvent = $ReadOnly<{ - altKey?: ?boolean, // [macOS] - button?: ?number, // [macOS] /** * Array of all touch events that have changed since the last event */ changedTouches: $ReadOnlyArray, - ctrlKey?: ?boolean, // [macOS] /** * 3D Touch reported force * @platform ios @@ -245,7 +242,7 @@ export type NativeTouchEvent = $ReadOnly<{ * The Y position of the touch, relative to the element */ locationY: number, - metaKey?: ?boolean, // [macOS] + /** * The X position of the touch, relative to the screen */ @@ -254,7 +251,6 @@ export type NativeTouchEvent = $ReadOnly<{ * The Y position of the touch, relative to the screen */ pageY: number, - shiftKey?: ?boolean, // [macOS] /** * The node id of the element receiving the touch event */ @@ -267,6 +263,13 @@ export type NativeTouchEvent = $ReadOnly<{ * Array of all current touches on the screen */ touches: $ReadOnlyArray, + // [macOS + ctrlKey?: ?boolean, + altKey?: ?boolean, + shiftKey?: ?boolean, + metaKey?: ?boolean, + button?: ?number, + // macOS] }>; export type GestureResponderEvent = ResponderSyntheticEvent; @@ -283,48 +286,6 @@ export type NativeScrollPoint = $ReadOnly<{ x: number, }>; -// [macOS -export type KeyEvent = NativeSyntheticEvent< - $ReadOnly<{| - // Modifier keys - capsLockKey: boolean, - shiftKey: boolean, - ctrlKey: boolean, - altKey: boolean, - metaKey: boolean, - numericPadKey: boolean, - helpKey: boolean, - functionKey: boolean, - // Key options - ArrowLeft: boolean, - ArrowRight: boolean, - ArrowUp: boolean, - ArrowDown: boolean, - key: string, - |}>, ->; - -/** - * Represents a key that could be passed to `KeyDownEvents` and `KeyUpEvents`. - * - * `key` is the actual key, such as "a", or one of the special values: - * "Tab", "Escape", "Enter", "ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", - * "Backspace", "Delete", "Home", "End", "PageUp", "PageDown". - * - * The rest are modifiers that when absent mean false. - * - * @platform macos - */ -export type HandledKeyEvent = $ReadOnly<{| - altKey?: ?boolean, - ctrlKey?: ?boolean, - metaKey?: ?boolean, - shiftKey?: ?boolean, - key: string, -|}>; - -// macOS] - export type NativeScrollVelocity = $ReadOnly<{ y: number, x: number, @@ -370,3 +331,70 @@ export type MouseEvent = NativeSyntheticEvent< timestamp: number, }>, >; + +// [macOS +export type DataTransferItem = $ReadOnly<{ + name: string, + kind: string, + type: string, + uri: string, + size?: number, + width?: number, + height?: number, +}>; + +export type DataTransfer = $ReadOnly<{ + files: $ReadOnlyArray, + types: $ReadOnlyArray, +}>; + +export type DragEvent = NativeSyntheticEvent< + $ReadOnly<{ + clientX: number, + clientY: number, + pageX: number, + pageY: number, + timestamp: number, + dataTransfer?: DataTransfer, + }>, +>; + +export type KeyEvent = NativeSyntheticEvent< + $ReadOnly<{| + // Modifier keys + capsLockKey: boolean, + shiftKey: boolean, + ctrlKey: boolean, + altKey: boolean, + metaKey: boolean, + numericPadKey: boolean, + helpKey: boolean, + functionKey: boolean, + // Key options + ArrowLeft: boolean, + ArrowRight: boolean, + ArrowUp: boolean, + ArrowDown: boolean, + key: string, + |}>, +>; + +/** + * Represents a key that could be passed to `KeyDownEvents` and `KeyUpEvents`. + * + * `key` is the actual key, such as "a", or one of the special values: + * "Tab", "Escape", "Enter", "ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", + * "Backspace", "Delete", "Home", "End", "PageUp", "PageDown". + * + * The rest are modifiers that when absent mean false. + * + * @platform macos + */ +export type HandledKeyEvent = $ReadOnly<{| + ctrlKey?: ?boolean, + altKey?: ?boolean, + shiftKey?: ?boolean, + metaKey?: ?boolean, + key: string, +|}>; +// macOS] diff --git a/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap b/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap index 0938ea54dbfdc6..c00e8ab99622f3 100644 --- a/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap +++ b/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap @@ -1744,9 +1744,9 @@ type PressableBaseProps = $ReadOnly<{ enableFocusRing?: ?boolean, allowsVibrancy?: ?boolean, tooltip?: ?string, - onDragEnter?: (event: MouseEvent) => void, - onDragLeave?: (event: MouseEvent) => void, - onDrop?: (event: MouseEvent) => void, + onDragEnter?: (event: DragEvent) => void, + onDragLeave?: (event: DragEvent) => void, + onDrop?: (event: DragEvent) => void, draggedTypes?: ?DraggedTypesType, style?: | ViewStyleProp @@ -3774,9 +3774,9 @@ exports[`public API should not change unintentionally Libraries/Components/Touch tooltip?: ?string, onMouseEnter?: (event: MouseEvent) => void, onMouseLeave?: (event: MouseEvent) => void, - onDragEnter?: (event: MouseEvent) => void, - onDragLeave?: (event: MouseEvent) => void, - onDrop?: (event: MouseEvent) => void, + onDragEnter?: (event: DragEvent) => void, + onDragLeave?: (event: DragEvent) => void, + onDrop?: (event: DragEvent) => void, draggedTypes?: ?DraggedTypesType, }; export type TouchableWithoutFeedbackPropsAndroid = { @@ -3841,7 +3841,7 @@ declare export default typeof UnimplementedView; `; exports[`public API should not change unintentionally Libraries/Components/View/DraggedType.js 1`] = ` -"export type DraggedType = \\"fileUrl\\"; +"export type DraggedType = \\"fileUrl\\" | \\"image\\" | \\"string\\"; export type DraggedTypesType = DraggedType | $ReadOnlyArray; declare module.exports: { DraggedTypes: $FlowFixMe }; " @@ -4212,9 +4212,9 @@ export type ViewPropsIOS = $ReadOnly<{ shouldRasterizeIOS?: ?boolean, }>; type MacOSViewProps = $ReadOnly<{| - onDragEnter?: (event: MouseEvent) => void, - onDragLeave?: (event: MouseEvent) => void, - onDrop?: (event: MouseEvent) => void, + onDragEnter?: (event: DragEvent) => void, + onDragLeave?: (event: DragEvent) => void, + onDrop?: (event: DragEvent) => void, tooltip?: ?string, acceptsFirstMouse?: ?boolean, mouseDownCanMoveWindow?: ?boolean, @@ -8486,21 +8486,21 @@ export interface NativePointerEvent extends NativeMouseEvent { } export type PointerEvent = NativeSyntheticEvent; export type NativeTouchEvent = $ReadOnly<{ - altKey?: ?boolean, - button?: ?number, changedTouches: $ReadOnlyArray, - ctrlKey?: ?boolean, force?: number, identifier: number, locationX: number, locationY: number, - metaKey?: ?boolean, pageX: number, pageY: number, - shiftKey?: ?boolean, target: ?number, timestamp: number, touches: $ReadOnlyArray, + ctrlKey?: ?boolean, + altKey?: ?boolean, + shiftKey?: ?boolean, + metaKey?: ?boolean, + button?: ?number, }>; export type GestureResponderEvent = ResponderSyntheticEvent; export type NativeScrollRectangle = $ReadOnly<{ @@ -8513,30 +8513,6 @@ export type NativeScrollPoint = $ReadOnly<{ y: number, x: number, }>; -export type KeyEvent = NativeSyntheticEvent< - $ReadOnly<{| - capsLockKey: boolean, - shiftKey: boolean, - ctrlKey: boolean, - altKey: boolean, - metaKey: boolean, - numericPadKey: boolean, - helpKey: boolean, - functionKey: boolean, - ArrowLeft: boolean, - ArrowRight: boolean, - ArrowUp: boolean, - ArrowDown: boolean, - key: string, - |}>, ->; -export type HandledKeyEvent = $ReadOnly<{| - altKey?: ?boolean, - ctrlKey?: ?boolean, - metaKey?: ?boolean, - shiftKey?: ?boolean, - key: string, -|}>; export type NativeScrollVelocity = $ReadOnly<{ y: number, x: number, @@ -8572,6 +8548,53 @@ export type MouseEvent = NativeSyntheticEvent< timestamp: number, }>, >; +export type DataTransferItem = $ReadOnly<{ + name: string, + kind: string, + type: string, + uri: string, + size?: number, + width?: number, + height?: number, +}>; +export type DataTransfer = $ReadOnly<{ + files: $ReadOnlyArray, + types: $ReadOnlyArray, +}>; +export type DragEvent = NativeSyntheticEvent< + $ReadOnly<{ + clientX: number, + clientY: number, + pageX: number, + pageY: number, + timestamp: number, + dataTransfer?: DataTransfer, + }>, +>; +export type KeyEvent = NativeSyntheticEvent< + $ReadOnly<{| + capsLockKey: boolean, + shiftKey: boolean, + ctrlKey: boolean, + altKey: boolean, + metaKey: boolean, + numericPadKey: boolean, + helpKey: boolean, + functionKey: boolean, + ArrowLeft: boolean, + ArrowRight: boolean, + ArrowUp: boolean, + ArrowDown: boolean, + key: string, + |}>, +>; +export type HandledKeyEvent = $ReadOnly<{| + ctrlKey?: ?boolean, + altKey?: ?boolean, + shiftKey?: ?boolean, + metaKey?: ?boolean, + key: string, +|}>; " `; diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm index 23ab6549b2a89f..d3defbfbb9c6ca 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm @@ -11,6 +11,9 @@ #import #import #import +#if TARGET_OS_OSX // [macOS +#import +#endif // macOS] #import #import @@ -32,6 +35,7 @@ #if TARGET_OS_OSX // [macOS #import #import +#import #endif // macOS] using namespace facebook::react; @@ -588,7 +592,29 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & needsInvalidateLayer = YES; } -#if TARGET_OS_OSX // [macOS] +#if TARGET_OS_OSX // [macOS + // `draggedTypes` + if (oldViewProps.draggedTypes != newViewProps.draggedTypes) { + if (!oldViewProps.draggedTypes.empty()) { + [self unregisterDraggedTypes]; + } + + if (!newViewProps.draggedTypes.empty()) { + NSMutableArray *pasteboardTypes = [NSMutableArray arrayWithCapacity:newViewProps.draggedTypes.size()]; + for (const auto &draggedType : newViewProps.draggedTypes) { + if (draggedType == "fileUrl") { + [pasteboardTypes addObject:NSFilenamesPboardType]; + } else if (draggedType == "image") { + [pasteboardTypes addObject:NSPasteboardTypePNG]; + [pasteboardTypes addObject:NSPasteboardTypeTIFF]; + } else if (draggedType == "string") { + [pasteboardTypes addObject:NSPasteboardTypeString]; + } + } + [self registerForDraggedTypes:pasteboardTypes]; + } + } + // `tooltip` if (oldViewProps.tooltip != newViewProps.tooltip) { if (newViewProps.tooltip.has_value()) { @@ -597,7 +623,7 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & self.toolTip = nil; } } -#endif // macOS] +#endif // [macOS] _needsInvalidateLayer = _needsInvalidateLayer || needsInvalidateLayer; @@ -1680,6 +1706,155 @@ - (void)keyUp:(NSEvent *)event { } +#pragma mark - Drag and Drop Events + +enum DragEventType { + DragEnter, + DragLeave, + Drop, +}; + +- (void)buildDataTransferItems:(std::vector &)dataTransferItems forPasteboard:(NSPasteboard *)pasteboard { + NSArray *fileNames = [pasteboard propertyListForType:NSFilenamesPboardType] ?: @[]; + for (NSString *file in fileNames) { + NSURL *fileURL = [NSURL fileURLWithPath:file]; + BOOL isDir = NO; + BOOL isValid = [[NSFileManager defaultManager] fileExistsAtPath:fileURL.path isDirectory:&isDir] && !isDir; + if (isValid) { + + NSString *MIMETypeString = nil; + if (fileURL.pathExtension) { + CFStringRef fileExtension = (__bridge CFStringRef)fileURL.pathExtension; + CFStringRef UTI = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, fileExtension, NULL); + if (UTI != NULL) { + CFStringRef MIMEType = UTTypeCopyPreferredTagWithClass(UTI, kUTTagClassMIMEType); + CFRelease(UTI); + MIMETypeString = (__bridge_transfer NSString *)MIMEType; + } + } + + NSNumber *fileSizeValue = nil; + NSError *fileSizeError = nil; + BOOL success = [fileURL getResourceValue:&fileSizeValue + forKey:NSURLFileSizeKey + error:&fileSizeError]; + + DataTransferItem transferItem = { + .name = fileURL.lastPathComponent ? fileURL.lastPathComponent.UTF8String : "", + .kind = "file", + .type = MIMETypeString ? MIMETypeString.UTF8String : "", + .uri = fileURL.path ? fileURL.path.UTF8String : "", + }; + + if (success) { + transferItem.size = fileSizeValue.intValue; + } + + if ([MIMETypeString hasPrefix:@"image/"]) { + NSImage *image = [[NSImage alloc] initWithContentsOfURL:fileURL]; + CGImageRef cgImage = [image CGImageForProposedRect:nil context:nil hints:nil]; + transferItem.width = static_cast(CGImageGetWidth(cgImage)); + transferItem.height = static_cast(CGImageGetHeight(cgImage)); + } + + dataTransferItems.push_back(transferItem); + } + } + + NSPasteboardType imageType = [pasteboard availableTypeFromArray:@[NSPasteboardTypePNG, NSPasteboardTypeTIFF]]; + if (imageType && fileNames.count == 0) { + NSString *MIMETypeString = imageType == NSPasteboardTypePNG ?[UTTypePNG preferredMIMEType] : [UTTypeTIFF preferredMIMEType]; + NSData *imageData = [pasteboard dataForType:imageType]; + NSImage *image = [[NSImage alloc] initWithData:imageData]; + CGImageRef cgImage = [image CGImageForProposedRect:nil context:nil hints:nil]; + + NSString *dataURLString = RCTDataURL(MIMETypeString, imageData).absoluteString; + + DataTransferItem transferItem = { + .kind = "image", + .type = MIMETypeString ? MIMETypeString.UTF8String : "", + .uri = dataURLString ? dataURLString.UTF8String : "", + .size = static_cast(imageData.length), + .width = static_cast(CGImageGetWidth(cgImage)), + .height = static_cast(CGImageGetHeight(cgImage)), + }; + + dataTransferItems.push_back(transferItem); + } +} + +- (void)emitDragEvent:(DragEventType)eventType draggingInfo:(id)sender { + if (!_eventEmitter) { + return; + } + + NSPoint locationInWindow = sender.draggingLocation; + NSPasteboard *pasteboard = sender.draggingPasteboard; + + std::vector dataTransferItems{}; + [self buildDataTransferItems:dataTransferItems forPasteboard:pasteboard]; + + NSPoint locationInView = [self convertPoint:locationInWindow fromView:nil]; + NSEventModifierFlags modifierFlags = self.window.currentEvent.modifierFlags; + + DragEvent dragEvent = { + { + .clientX = locationInView.x, + .clientY = locationInView.y, + .screenX = locationInWindow.x, + .screenY = locationInWindow.y, + .altKey = static_cast(modifierFlags & NSEventModifierFlagOption), + .ctrlKey = static_cast(modifierFlags & NSEventModifierFlagControl), + .shiftKey = static_cast(modifierFlags & NSEventModifierFlagShift), + .metaKey = static_cast(modifierFlags & NSEventModifierFlagCommand), + }, + .dataTransferItems = dataTransferItems, + }; + + switch (eventType) { + case DragEnter: + _eventEmitter->onDragEnter(dragEvent); + break; + + case DragLeave: + _eventEmitter->onDragLeave(dragEvent); + break; + + case Drop: + _eventEmitter->onDrop(dragEvent); + break; + } +} + +- (NSDragOperation)draggingEntered:(id )sender +{ + NSPasteboard *pboard = sender.draggingPasteboard; + NSDragOperation sourceDragMask = sender.draggingSourceOperationMask; + + [self emitDragEvent:DragEnter draggingInfo:sender]; + + if ([pboard availableTypeFromArray:self.registeredDraggedTypes]) { + if (sourceDragMask & NSDragOperationLink) { + return NSDragOperationLink; + } else if (sourceDragMask & NSDragOperationCopy) { + return NSDragOperationCopy; + } + } + return NSDragOperationNone; +} + +- (void)draggingExited:(id)sender +{ + [self emitDragEvent:DragLeave draggingInfo:sender]; +} + +- (BOOL)performDragOperation:(id )sender +{ + [self emitDragEvent:Drop draggingInfo:sender]; + return YES; +} + + #pragma mark - Mouse Events - (void)emitMouseEvent { diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewEventEmitter.cpp b/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewEventEmitter.cpp index f87f4eb37b8b34..0be170a1dafbae 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewEventEmitter.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewEventEmitter.cpp @@ -52,7 +52,8 @@ void HostPlatformViewEventEmitter::onKeyUp(const KeyEvent& keyEvent) const { #pragma mark - Mouse Events -static jsi::Value mouseEventPayload(jsi::Runtime& runtime, const MouseEvent& event) { +// Returns an Object instead of value as we read and modify it in dragEventPayload. +static jsi::Object mouseEventPayload(jsi::Runtime& runtime, const MouseEvent& event) { auto payload = jsi::Object(runtime); payload.setProperty(runtime, "clientX", event.clientX); payload.setProperty(runtime, "clientY", event.clientY); @@ -77,4 +78,74 @@ void HostPlatformViewEventEmitter::onMouseLeave(const MouseEvent& mouseEvent) co }); } +#pragma mark - Drag and Drop Events + +static jsi::Value dataTransferPayload( + jsi::Runtime& runtime, + const std::vector& dataTransferItems) { + auto filesArray = jsi::Array(runtime, dataTransferItems.size()); + auto itemsArray = jsi::Array(runtime, dataTransferItems.size()); + auto typesArray = jsi::Array(runtime, dataTransferItems.size()); + int i = 0; + for (const auto& transferItem : dataTransferItems) { + auto fileObject = jsi::Object(runtime); + fileObject.setProperty(runtime, "name", transferItem.name); + fileObject.setProperty(runtime, "type", transferItem.type); + fileObject.setProperty(runtime, "uri", transferItem.uri); + if (transferItem.size.has_value()) { + fileObject.setProperty(runtime, "size", *transferItem.size); + } + if (transferItem.width.has_value()) { + fileObject.setProperty(runtime, "width", *transferItem.width); + } + if (transferItem.height.has_value()) { + fileObject.setProperty(runtime, "height", *transferItem.height); + } + filesArray.setValueAtIndex(runtime, i, fileObject); + + auto itemObject = jsi::Object(runtime); + itemObject.setProperty(runtime, "kind", transferItem.kind); + itemObject.setProperty(runtime, "type", transferItem.type); + itemsArray.setValueAtIndex(runtime, i, itemObject); + + typesArray.setValueAtIndex(runtime, i, transferItem.type); + i++; + } + + auto dataTransferObject = jsi::Object(runtime); + dataTransferObject.setProperty(runtime, "files", filesArray); + dataTransferObject.setProperty(runtime, "items", itemsArray); + dataTransferObject.setProperty(runtime, "types", typesArray); + + return dataTransferObject; +} + +static jsi::Value dragEventPayload( + jsi::Runtime& runtime, + const DragEvent& event) { + auto payload = mouseEventPayload(runtime, event); + auto dataTransferObject = + dataTransferPayload(runtime, event.dataTransferItems); + payload.setProperty(runtime, "dataTransfer", dataTransferObject); + return payload; +} + +void HostPlatformViewEventEmitter::onDragEnter(DragEvent const& dragEvent) const { + dispatchEvent("dragEnter", [dragEvent](jsi::Runtime &runtime) { + return dragEventPayload(runtime, dragEvent); + }); +} + +void HostPlatformViewEventEmitter::onDragLeave(DragEvent const& dragEvent) const { + dispatchEvent("dragLeave", [dragEvent](jsi::Runtime &runtime) { + return dragEventPayload(runtime, dragEvent); + }); +} + +void HostPlatformViewEventEmitter::onDrop(DragEvent const& dragEvent) const { + dispatchEvent("drop", [dragEvent](jsi::Runtime &runtime) { + return dragEventPayload(runtime, dragEvent); + }); +} + } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewEventEmitter.h b/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewEventEmitter.h index ff6c31a3673ad4..69d282bbf81a10 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewEventEmitter.h +++ b/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewEventEmitter.h @@ -33,6 +33,12 @@ class HostPlatformViewEventEmitter : public BaseViewEventEmitter { void onMouseEnter(MouseEvent const& mouseEvent) const; void onMouseLeave(MouseEvent const& mouseEvent) const; + +#pragma mark - Drag and Drop Events + + void onDragEnter(DragEvent const& dragEvent) const; + void onDragLeave(DragEvent const& dragEvent) const; + void onDrop(DragEvent const& dragEvent) const; }; -} // namespace facebook::react \ No newline at end of file +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewProps.cpp b/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewProps.cpp index 8b4d4a6bb5097a..2ef2fdd7dfb313 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewProps.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewProps.cpp @@ -66,6 +66,15 @@ HostPlatformViewProps::HostPlatformViewProps( "keyUpEvents", sourceProps.keyUpEvents, {})), + draggedTypes( + ReactNativeFeatureFlags::enableCppPropsIteratorSetter() + ? sourceProps.draggedTypes + : convertRawProp( + context, + rawProps, + "draggedTypes", + sourceProps.draggedTypes, + {})), tooltip( ReactNativeFeatureFlags::enableCppPropsIteratorSetter() ? sourceProps.tooltip @@ -111,6 +120,7 @@ void HostPlatformViewProps::setProp( RAW_SET_PROP_SWITCH_CASE_BASIC(enableFocusRing); RAW_SET_PROP_SWITCH_CASE_BASIC(keyDownEvents); RAW_SET_PROP_SWITCH_CASE_BASIC(keyUpEvents); + RAW_SET_PROP_SWITCH_CASE_BASIC(draggedTypes); RAW_SET_PROP_SWITCH_CASE_BASIC(tooltip); } } diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewProps.h b/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewProps.h index c2b0c3acf7ddff..27f0e3d1e30a9a 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewProps.h +++ b/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewProps.h @@ -47,6 +47,8 @@ class HostPlatformViewProps : public BaseViewProps { std::vector keyDownEvents{}; std::vector keyUpEvents{}; + std::vector draggedTypes{}; + std::optional tooltip{}; }; } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/MouseEvent.h b/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/MouseEvent.h index 457083eec2fc4e..aafa96e7917ec7 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/MouseEvent.h +++ b/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/MouseEvent.h @@ -56,4 +56,18 @@ struct MouseEvent { bool metaKey{false}; }; +struct DataTransferItem { + std::string name{}; + std::string kind{}; + std::string type{}; + std::string uri{}; + std::optional size{}; + std::optional width{}; + std::optional height{}; +}; + +struct DragEvent : MouseEvent { + std::vector dataTransferItems; +}; + } // namespace facebook::react diff --git a/packages/rn-tester/js/examples/DragAndDrop/DragAndDropExample.js b/packages/rn-tester/js/examples/DragAndDrop/DragAndDropExample.js new file mode 100644 index 00000000000000..c873886ac65dc9 --- /dev/null +++ b/packages/rn-tester/js/examples/DragAndDrop/DragAndDropExample.js @@ -0,0 +1,230 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @flow + */ + +'use strict'; + +// [macOS + +import type {PasteEvent} from 'react-native/Libraries/Components/TextInput/TextInput'; + +import ExampleTextInput from '../TextInput/ExampleTextInput'; +import React from 'react'; +import {Image, StyleSheet, Text, View} from 'react-native'; + +const styles = StyleSheet.create({ + multiline: { + height: 50, + }, +}); + +function DragDropView(): React.Node { + // $FlowFixMe[missing-empty-array-annot] + const [log, setLog] = React.useState([]); + const appendLog = (line: string) => { + const limit = 6; + let newLog = log.slice(0, limit - 1); + newLog.unshift(line); + setLog(newLog); + }; + const [imageUri, setImageUri] = React.useState( + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z/C/HgAGgwJ/lK3Q6wAAAABJRU5ErkJggg==', + ); + const [isDraggingOver, setIsDraggingOver] = React.useState(false); + + return ( + <> + { + appendLog('onDragEnter'); + setIsDraggingOver(true); + }} + onDragLeave={e => { + appendLog('onDragLeave'); + setIsDraggingOver(false); + }} + onDrop={e => { + appendLog('onDrop'); + setIsDraggingOver(false); + const file = e.nativeEvent.dataTransfer?.files?.[0]; + if (file) { + if (file.type.startsWith('image/')) { + appendLog('Dropped image file: ' + file.name); + } else { + appendLog('Dropped file: ' + file.name); + } + setImageUri(file.uri); + } + }} + style={{ + height: 150, + backgroundColor: isDraggingOver ? '#e3f2fd' : '#f0f0f0', + borderWidth: 2, + borderColor: isDraggingOver ? '#2196f3' : '#0066cc', + borderStyle: 'dashed', + borderRadius: 8, + justifyContent: 'center', + alignItems: 'center', + marginVertical: 10, + }}> + + {isDraggingOver ? 'Release to drop' : 'Drop an image or file here'} + + + + + Event Log: + {log.join('\n')} + + + + Dropped Image: + + + + + + ); +} + +function OnDragEnterOnDragLeaveOnDrop(): React.Node { + // $FlowFixMe[missing-empty-array-annot] + const [log, setLog] = React.useState([]); + const appendLog = (line: string) => { + const limit = 6; + let newLog = log.slice(0, limit - 1); + newLog.unshift(line); + setLog(newLog); + }; + return ( + <> + appendLog('SinglelineEnter')} + onDragLeave={e => appendLog('SinglelineLeave')} + onDrop={e => appendLog('SinglelineDrop')} + style={styles.multiline} + placeholder="SINGLE LINE with onDragEnter|Leave() and onDrop()" + /> + appendLog('MultilineEnter')} + onDragLeave={e => appendLog('MultilineLeave')} + onDrop={e => appendLog('MultilineDrop')} + style={styles.multiline} + placeholder="MULTI LINE with onDragEnter|Leave() and onDrop()" + /> + {log.join('\n')} + + + + ); +} + +function OnPaste(): React.Node { + // $FlowFixMe[missing-empty-array-annot] + const [log, setLog] = React.useState([]); + const appendLog = (line: string) => { + const limit = 3; + let newLog = log.slice(0, limit - 1); + newLog.unshift(line); + setLog(newLog); + }; + const [imageUri, setImageUri] = React.useState( + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z/C/HgAGgwJ/lK3Q6wAAAABJRU5ErkJggg==', + ); + return ( + <> + { + appendLog(JSON.stringify(e.nativeEvent.dataTransfer.types)); + const file = e.nativeEvent.dataTransfer?.files?.[0]; + if (file) { + setImageUri(file.uri); + } + }} + pastedTypes={['string']} + placeholder="MULTI LINE with onPaste() text from clipboard" + /> + { + appendLog(JSON.stringify(e.nativeEvent.dataTransfer.types)); + const file = e.nativeEvent.dataTransfer?.files?.[0]; + if (file) { + setImageUri(file.uri); + } + }} + pastedTypes={['fileUrl', 'image', 'string']} + placeholder="MULTI LINE with onPaste() for PNG/TIFF images from clipboard or fileUrl (via Finder) and text from clipboard" + /> + {log.join('\n')} + + + ); +} + +exports.title = 'Drag and Drop Events'; +exports.category = 'UI'; +exports.description = + 'Demonstrates onDragEnter, onDragLeave, onDrop, and onPaste event handling in TextInput.'; +exports.examples = [ + { + title: 'Drag and Drop (View)', + render: function (): React.Node { + return ; + }, + }, + { + title: 'Drag and Drop (TextInput)', + render: function (): React.Node { + return ; + }, + }, + { + title: 'onPaste (MultiLineTextInput)', + render: function (): React.Node { + return ; + }, + }, +]; + +// macOS] diff --git a/packages/rn-tester/js/examples/Pressable/PressableExample.js b/packages/rn-tester/js/examples/Pressable/PressableExample.js index 4a25c165847b19..3c853ef1e12e4b 100644 --- a/packages/rn-tester/js/examples/Pressable/PressableExample.js +++ b/packages/rn-tester/js/examples/Pressable/PressableExample.js @@ -127,6 +127,10 @@ function PressableFeedbackEvents() { onHoverOut={() => appendEvent('hoverOut')} onFocus={() => appendEvent('focus')} onBlur={() => appendEvent('blur')} + onDragEnter={() => appendEvent('dragEnter')} + onDragLeave={() => appendEvent('dragLeave')} + onDrop={() => appendEvent('drop')} + draggedTypes={'fileUrl'} // macOS] onPress={() => appendEvent('press')} onPressIn={() => appendEvent('pressIn')} diff --git a/packages/rn-tester/js/examples/TextInput/TextInputExample.ios.js b/packages/rn-tester/js/examples/TextInput/TextInputExample.ios.js index 909d870acb0797..cd1ab2d1f758c5 100644 --- a/packages/rn-tester/js/examples/TextInput/TextInputExample.ios.js +++ b/packages/rn-tester/js/examples/TextInput/TextInputExample.ios.js @@ -15,11 +15,7 @@ import type { RNTesterModuleExample, } from '../../types/RNTesterTypes'; import type {KeyboardTypeOptions} from 'react-native/Libraries/Components/TextInput/TextInput'; -// [macOS -import type { - PasteEvent, - SettingChangeEvent, -} from 'react-native/Libraries/Components/TextInput/TextInput'; // macOS] +import type {SettingChangeEvent} from 'react-native/Libraries/Components/TextInput/TextInput'; // [macOS] import RNTesterText from '../../components/RNTesterText'; import ExampleTextInput from './ExampleTextInput'; @@ -29,7 +25,6 @@ import { Alert, Button, InputAccessoryView, - Image, // [macOS] Platform, // [macOS] StyleSheet, Switch, @@ -388,99 +383,6 @@ function SpellingAndGrammarEvents(): React.Node { ); } - -function OnDragEnterOnDragLeaveOnDrop(): React.Node { - // $FlowFixMe[missing-empty-array-annot] - const [log, setLog] = React.useState([]); - const appendLog = (line: string) => { - const limit = 6; - let newLog = log.slice(0, limit - 1); - newLog.unshift(line); - setLog(newLog); - }; - return ( - <> - appendLog('SinglelineEnter')} - onDragLeave={e => appendLog('SinglelineLeave')} - onDrop={e => appendLog('SinglelineDrop')} - style={styles.multiline} - placeholder="SINGLE LINE with onDragEnter|Leave() and onDrop()" - /> - appendLog('MultilineEnter')} - onDragLeave={e => appendLog('MultilineLeave')} - onDrop={e => appendLog('MultilineDrop')} - style={styles.multiline} - placeholder="MULTI LINE with onDragEnter|Leave() and onDrop()" - /> - {log.join('\n')} - - - - ); -} - -function OnPaste(): React.Node { - // $FlowFixMe[missing-empty-array-annot] - const [log, setLog] = React.useState([]); - const appendLog = (line: string) => { - const limit = 3; - let newLog = log.slice(0, limit - 1); - newLog.unshift(line); - setLog(newLog); - }; - const [imageUri, setImageUri] = React.useState( - 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z/C/HgAGgwJ/lK3Q6wAAAABJRU5ErkJggg==', - ); - return ( - <> - { - appendLog(JSON.stringify(e.nativeEvent.dataTransfer.types)); - setImageUri(e.nativeEvent.dataTransfer.files[0].uri); - }} - pastedTypes={['string']} - placeholder="MULTI LINE with onPaste() text from clipboard" - /> - { - appendLog(JSON.stringify(e.nativeEvent.dataTransfer.types)); - setImageUri(e.nativeEvent.dataTransfer.files[0].uri); - }} - pastedTypes={['fileUrl', 'image', 'string']} - placeholder="MULTI LINE with onPaste() for PNG/TIFF images from clipboard or fileUrl (via Finder) and text from clipboard" - /> - {log.join('\n')} - - - ); -} // macOS] const textInputExamples: Array = [ @@ -1233,19 +1135,6 @@ if (Platform.OS === 'macos') { ); }, }, - { - title: - 'onDragEnter, onDragLeave and onDrop - Single- & MultiLineTextInput', - render: function (): React.Node { - return ; - }, - }, - { - title: 'onPaste - MultiLineTextInput', - render: function (): React.Node { - return ; - }, - }, { title: 'Cursor color', render: function (): React.Node { diff --git a/packages/rn-tester/js/utils/RNTesterList.ios.js b/packages/rn-tester/js/utils/RNTesterList.ios.js index 87a92d6de5f18f..7878fe2a8fe67b 100644 --- a/packages/rn-tester/js/utils/RNTesterList.ios.js +++ b/packages/rn-tester/js/utils/RNTesterList.ios.js @@ -253,6 +253,11 @@ const APIs: Array = ([ module: require('../examples/DisplayContents/DisplayContentsExample') .default, }, + { + key: 'DragAndDropExample', + category: 'UI', + module: require('../examples/DragAndDrop/DragAndDropExample'), + }, // Only show the link for the example if the API is available. // $FlowExpectedError[cannot-resolve-name] typeof IntersectionObserver === 'function'