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/Text/TextInput/RCTBackedTextInputViewProtocol.h b/packages/react-native/Libraries/Text/TextInput/RCTBackedTextInputViewProtocol.h index 2a2f14e8d69770..8d3431ac598573 100644 --- a/packages/react-native/Libraries/Text/TextInput/RCTBackedTextInputViewProtocol.h +++ b/packages/react-native/Libraries/Text/TextInput/RCTBackedTextInputViewProtocol.h @@ -88,6 +88,7 @@ NS_ASSUME_NONNULL_BEGIN #if TARGET_OS_OSX // [macOS // UITextInput method for OSX - (CGSize)sizeThatFits:(CGSize)size; +- (void)setReadablePasteBoardTypes:(NSArray *)readablePasteboardTypes; #endif // macOS] // This protocol disallows direct access to `text` property because diff --git a/packages/react-native/Libraries/Text/TextInput/Singleline/RCTUITextField.h b/packages/react-native/Libraries/Text/TextInput/Singleline/RCTUITextField.h index 1d0b193c46a96f..14735c2cd509eb 100644 --- a/packages/react-native/Libraries/Text/TextInput/Singleline/RCTUITextField.h +++ b/packages/react-native/Libraries/Text/TextInput/Singleline/RCTUITextField.h @@ -69,6 +69,8 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, strong, nullable) RCTUIColor *selectionColor; @property (weak, nullable) id delegate; @property (nonatomic, assign) CGFloat pointScaleFactor; + +- (void)setReadablePasteBoardTypes:(NSArray *)readablePasteboardTypes; #endif // macOS] @property (nonatomic, getter=isGhostTextChanging) BOOL ghostTextChanging; // [macOS] diff --git a/packages/react-native/Libraries/Text/TextInput/Singleline/RCTUITextField.mm b/packages/react-native/Libraries/Text/TextInput/Singleline/RCTUITextField.mm index dd693c7b4f8574..ea73d98628da3f 100644 --- a/packages/react-native/Libraries/Text/TextInput/Singleline/RCTUITextField.mm +++ b/packages/react-native/Libraries/Text/TextInput/Singleline/RCTUITextField.mm @@ -99,6 +99,7 @@ @implementation RCTUITextField { #endif // [macOS] #if TARGET_OS_OSX // [macOS BOOL _isUpdatingPlaceholderText; + NSArray *_readablePasteboardTypes; #endif // macOS] } @@ -705,5 +706,12 @@ - (void)keyUp:(NSEvent *)event { } } #endif // macOS] + +#if TARGET_OS_OSX // [macOS +- (void)setReadablePasteBoardTypes:(NSArray *)readablePasteboardTypes +{ + _readablePasteboardTypes = readablePasteboardTypes; +} +#endif // macOS] @end 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/TextInput/RCTTextInputComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm index 391517373bdb7e..7795af88057bad 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm @@ -344,6 +344,13 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & _backedTextInputView.disableKeyboardShortcuts = newTextInputProps.disableKeyboardShortcuts; } +#if TARGET_OS_OSX // [macOS + if (newTextInputProps.traits.pastedTypes!= oldTextInputProps.traits.pastedTypes) { + NSArray *types = RCTPasteboardTypeArrayFromProps(newTextInputProps.traits.pastedTypes); + [_backedTextInputView setReadablePasteBoardTypes:types]; + } +#endif // macOS] + [super updateProps:props oldProps:oldProps]; #if TARGET_OS_IOS // [macOS] [visionOS] @@ -563,40 +570,60 @@ - (void)grammarCheckingDidChange:(BOOL)enabled } } -- (BOOL)hasValidKeyDownOrValidKeyUp:(nonnull NSString *)key { - return YES; -} - - (void)submitOnKeyDownIfNeeded:(nonnull NSEvent *)event {} - (void)textInputDidCancel {} - (NSDragOperation)textInputDraggingEntered:(nonnull id)draggingInfo { + if ([draggingInfo.draggingPasteboard availableTypeFromArray:self.registeredDraggedTypes]) { + return [self draggingEntered:draggingInfo]; + } return NSDragOperationNone; } - (void)textInputDraggingExited:(nonnull id)draggingInfo { - return; + if ([draggingInfo.draggingPasteboard availableTypeFromArray:self.registeredDraggedTypes]) { + [self draggingExited:draggingInfo]; + } } -- (BOOL)textInputShouldHandleDeleteBackward:(nonnull id)sender { +- (BOOL)textInputShouldHandleDragOperation:(nonnull id)draggingInfo { + if ([draggingInfo.draggingPasteboard availableTypeFromArray:self.registeredDraggedTypes]) { + [self performDragOperation:draggingInfo]; + return NO; + } + return YES; } -- (BOOL)textInputShouldHandleDeleteForward:(nonnull id)sender { +- (BOOL)textInputShouldHandleDeleteBackward:(nonnull id)sender { return YES; } -- (BOOL)textInputShouldHandleDragOperation:(nonnull id)draggingInfo { +- (BOOL)textInputShouldHandleDeleteForward:(nonnull id)sender { return YES; } - (BOOL)textInputShouldHandleKeyEvent:(nonnull NSEvent *)event { - return YES; + return ![self handleKeyboardEvent:event]; } - (BOOL)textInputShouldHandlePaste:(nonnull id)sender { - return YES; + NSPasteboard *pasteboard = [NSPasteboard generalPasteboard]; + NSPasteboardType fileType = [pasteboard availableTypeFromArray:@[NSFilenamesPboardType, NSPasteboardTypePNG, NSPasteboardTypeTIFF]]; + NSArray* pastedTypes = ((RCTUITextView*) _backedTextInputView).readablePasteboardTypes; + + // If there's a fileType that is of interest, notify JS. Also blocks notifying JS if it's a text paste + if (_eventEmitter && fileType != nil && [pastedTypes containsObject:fileType]) { + auto const &textInputEventEmitter = *std::static_pointer_cast(_eventEmitter); + std::vector dataTransferItems{}; + [self buildDataTransferItems:dataTransferItems forPasteboard:pasteboard]; + + textInputEventEmitter.onPaste({.dataTransferItems = dataTransferItems}); + } + + // Only allow pasting text. + return fileType == nil; } #endif // macOS] diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.h b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.h index 76dc6d740d04c2..9de4f6ae4b694e 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.h +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.h @@ -17,6 +17,10 @@ #import #import +#if TARGET_OS_OSX // [macOS +#include +#endif // macOS] + NS_ASSUME_NONNULL_BEGIN /** @@ -79,6 +83,11 @@ NS_ASSUME_NONNULL_BEGIN - (void)finalizeUpdates:(RNComponentViewUpdateMask)updateMask NS_REQUIRES_SUPER; - (void)prepareForRecycle NS_REQUIRES_SUPER; +#if TARGET_OS_OSX // [macOS +- (BOOL)handleKeyboardEvent:(NSEvent *)event; +- (void)buildDataTransferItems:(std::vector &)dataTransferItems forPasteboard:(NSPasteboard *)pasteboard; +#endif // macOS] + /* * This is a fragment of temporary workaround that we need only temporary and will get rid of soon. */ 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..454be5ada69feb 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()) { @@ -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/React/Fabric/RCTConversions.h b/packages/react-native/React/Fabric/RCTConversions.h index ed9221e59d29be..b27a4390fd0e92 100644 --- a/packages/react-native/React/Fabric/RCTConversions.h +++ b/packages/react-native/React/Fabric/RCTConversions.h @@ -14,6 +14,10 @@ #import #import +#if TARGET_OS_OSX // [macOS +#import +#endif // macOS] + NS_ASSUME_NONNULL_BEGIN inline NSString *RCTNSStringFromString( @@ -281,4 +285,29 @@ inline facebook::react::LayoutDirection RCTLayoutDirection(BOOL isRTL) return isRTL ? facebook::react::LayoutDirection::RightToLeft : facebook::react::LayoutDirection::LeftToRight; } +#if TARGET_OS_OSX // [macOS +inline NSArray *RCTPasteboardTypeArrayFromProps(const std::vector &pastedTypes) +{ + NSMutableArray *types = [NSMutableArray new]; + + for (const auto &type : pastedTypes) { + switch (type) { + case facebook::react::PastedTypesType::FileUrl: + [types addObjectsFromArray:@[NSFilenamesPboardType]]; + break; + case facebook::react::PastedTypesType::Image: + [types addObjectsFromArray:@[NSPasteboardTypePNG, NSPasteboardTypeTIFF]]; + break; + case facebook::react::PastedTypesType::String: + [types addObjectsFromArray:@[NSPasteboardTypeString]]; + break; + default: + break; + } + } + + return [types copy]; +} +#endif // macOS] + NS_ASSUME_NONNULL_END diff --git a/packages/react-native/ReactCommon/react/renderer/components/textinput/TextInputEventEmitter.cpp b/packages/react-native/ReactCommon/react/renderer/components/textinput/TextInputEventEmitter.cpp index e7cba377f01e81..ea21e7b7c2c993 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/textinput/TextInputEventEmitter.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/textinput/TextInputEventEmitter.cpp @@ -5,6 +5,8 @@ * LICENSE file in the root directory of this source tree. */ +#include + #include "TextInputEventEmitter.h" namespace facebook::react { @@ -191,6 +193,13 @@ void TextInputEventEmitter::onGrammarCheckChange( const Metrics& textInputMetrics) const { dispatchTextInputEvent("grammarCheckChange", textInputMetrics); } + +void TextInputEventEmitter::onPaste( + const Metrics& textInputMetrics) const { + dispatchEvent("paste", [textInputMetrics](jsi::Runtime& runtime) { + return dataTransferPayload(runtime, textInputMetrics.dataTransferItems); + }); +} #endif // macOS] void TextInputEventEmitter::dispatchTextInputEvent( diff --git a/packages/react-native/ReactCommon/react/renderer/components/textinput/TextInputEventEmitter.h b/packages/react-native/ReactCommon/react/renderer/components/textinput/TextInputEventEmitter.h index 7648d7930196ba..cf2daabdf54a97 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/textinput/TextInputEventEmitter.h +++ b/packages/react-native/ReactCommon/react/renderer/components/textinput/TextInputEventEmitter.h @@ -32,7 +32,8 @@ class TextInputEventEmitter : public ViewEventEmitter { bool autoCorrectEnabled; bool spellCheckEnabled; bool grammarCheckEnabled; -#endif // macOS] + std::vector dataTransferItems; +#endif // macOS] }; struct KeyPressMetrics { @@ -53,6 +54,7 @@ class TextInputEventEmitter : public ViewEventEmitter { void onAutoCorrectChange(const Metrics& textInputMetrics) const; void onSpellCheckChange(const Metrics& textInputMetrics) const; void onGrammarCheckChange(const Metrics& textInputMetrics) const; + void onPaste(const Metrics& textInputMetrics) const; #endif // macOS] private: @@ -65,4 +67,4 @@ class TextInputEventEmitter : public ViewEventEmitter { const Metrics& textInputMetrics) const; }; -} // namespace facebook::react \ No newline at end of file +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/conversions.h b/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/conversions.h index d57d3f24064a82..18622478c2103b 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/conversions.h +++ b/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/conversions.h @@ -217,4 +217,26 @@ inline void fromRawValue( abort(); } +#ifdef TARGET_OS_OSX // [macOS +inline void fromRawValue( + const PropsParserContext &context, + const RawValue &value, + PastedTypesType &result) { + auto string = (std::string)value; + if (string == "fileUrl") { + result = PastedTypesType::FileUrl; + return; + } + if (string == "image") { + result = PastedTypesType::Image; + return; + } + if (string == "string") { + result = PastedTypesType::String; + return; + } + abort(); +} +#endif // macOS] + } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/primitives.h b/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/primitives.h index d71d58e9bc26ac..69e29613f732ad 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/primitives.h +++ b/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/primitives.h @@ -83,6 +83,14 @@ class Selection final { int end{0}; }; +#if TARGET_OS_OSX // [macOS +enum class PastedTypesType { + FileUrl, + Image, + String, +}; +#endif // macOS] + /* * Controls features of text inputs. */ @@ -227,6 +235,13 @@ class TextInputTraits final { * Default value: `empty` (`null`). */ std::optional grammarCheck{}; + + /* + * List of pastable types + * macOS-only + * Default value: `empty list` + */ + std::vector pastedTypes{}; #endif // macOS] }; diff --git a/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/propsConversions.h b/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/propsConversions.h index f4f23a3c74842f..17cb5afdede458 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/propsConversions.h +++ b/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/propsConversions.h @@ -143,6 +143,12 @@ static TextInputTraits convertRawProp( "grammarCheck", sourceTraits.grammarCheck, defaultTraits.grammarCheck); + traits.pastedTypes = convertRawProp( + context, + rawProps, + "pastedTypes", + sourceTraits.pastedTypes, + defaultTraits.pastedTypes); #endif // macOS] traits.dataDetectorTypes = convertRawProp( 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..f6d9fc6f16abcc 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 @@ -1,80 +1,150 @@ -/* - * Copyright (c) Microsoft Corporation. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - - // [macOS] - -#include -#include - -namespace facebook::react { - -#pragma mark - Focus Events - -void HostPlatformViewEventEmitter::onFocus() const { - dispatchEvent("focus"); -} - -void HostPlatformViewEventEmitter::onBlur() const { - dispatchEvent("blur"); -} - -#pragma mark - Keyboard Events - -static jsi::Value keyEventPayload(jsi::Runtime& runtime, const KeyEvent& event) { - auto payload = jsi::Object(runtime); - payload.setProperty(runtime, "key", jsi::String::createFromUtf8(runtime, event.key)); - payload.setProperty(runtime, "ctrlKey", event.ctrlKey); - payload.setProperty(runtime, "shiftKey", event.shiftKey); - payload.setProperty(runtime, "altKey", event.altKey); - payload.setProperty(runtime, "metaKey", event.metaKey); - payload.setProperty(runtime, "capsLockKey", event.capsLockKey); - payload.setProperty(runtime, "numericPadKey", event.numericPadKey); - payload.setProperty(runtime, "helpKey", event.helpKey); - payload.setProperty(runtime, "functionKey", event.functionKey); - return payload; -}; - -void HostPlatformViewEventEmitter::onKeyDown(const KeyEvent& keyEvent) const { - dispatchEvent("keyDown", [keyEvent](jsi::Runtime& runtime) { - return keyEventPayload(runtime, keyEvent); - }); -} - -void HostPlatformViewEventEmitter::onKeyUp(const KeyEvent& keyEvent) const { - dispatchEvent("keyUp", [keyEvent](jsi::Runtime& runtime) { - return keyEventPayload(runtime, keyEvent); - }); -} - -#pragma mark - Mouse Events - -static jsi::Value mouseEventPayload(jsi::Runtime& runtime, const MouseEvent& event) { - auto payload = jsi::Object(runtime); - payload.setProperty(runtime, "clientX", event.clientX); - payload.setProperty(runtime, "clientY", event.clientY); - payload.setProperty(runtime, "screenX", event.screenX); - payload.setProperty(runtime, "screenY", event.screenY); - payload.setProperty(runtime, "altKey", event.altKey); - payload.setProperty(runtime, "ctrlKey", event.ctrlKey); - payload.setProperty(runtime, "shiftKey", event.shiftKey); - payload.setProperty(runtime, "metaKey", event.metaKey); - return payload; -}; - -void HostPlatformViewEventEmitter::onMouseEnter(const MouseEvent& mouseEvent) const { - dispatchEvent("mouseEnter", [mouseEvent](jsi::Runtime &runtime) { - return mouseEventPayload(runtime, mouseEvent); - }); -} - -void HostPlatformViewEventEmitter::onMouseLeave(const MouseEvent& mouseEvent) const { - dispatchEvent("mouseLeave", [mouseEvent](jsi::Runtime &runtime) { - return mouseEventPayload(runtime, mouseEvent); - }); -} - -} // namespace facebook::react +/* + * Copyright (c) Microsoft Corporation. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + + // [macOS] + +#include +#include + +namespace facebook::react { + +#pragma mark - Focus Events + +void HostPlatformViewEventEmitter::onFocus() const { + dispatchEvent("focus"); +} + +void HostPlatformViewEventEmitter::onBlur() const { + dispatchEvent("blur"); +} + +#pragma mark - Keyboard Events + +static jsi::Value keyEventPayload(jsi::Runtime& runtime, const KeyEvent& event) { + auto payload = jsi::Object(runtime); + payload.setProperty(runtime, "key", jsi::String::createFromUtf8(runtime, event.key)); + payload.setProperty(runtime, "ctrlKey", event.ctrlKey); + payload.setProperty(runtime, "shiftKey", event.shiftKey); + payload.setProperty(runtime, "altKey", event.altKey); + payload.setProperty(runtime, "metaKey", event.metaKey); + payload.setProperty(runtime, "capsLockKey", event.capsLockKey); + payload.setProperty(runtime, "numericPadKey", event.numericPadKey); + payload.setProperty(runtime, "helpKey", event.helpKey); + payload.setProperty(runtime, "functionKey", event.functionKey); + return payload; +}; + +void HostPlatformViewEventEmitter::onKeyDown(const KeyEvent& keyEvent) const { + dispatchEvent("keyDown", [keyEvent](jsi::Runtime& runtime) { + return keyEventPayload(runtime, keyEvent); + }); +} + +void HostPlatformViewEventEmitter::onKeyUp(const KeyEvent& keyEvent) const { + dispatchEvent("keyUp", [keyEvent](jsi::Runtime& runtime) { + return keyEventPayload(runtime, keyEvent); + }); +} + +#pragma mark - Mouse Events + +// 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); + payload.setProperty(runtime, "screenX", event.screenX); + payload.setProperty(runtime, "screenY", event.screenY); + payload.setProperty(runtime, "altKey", event.altKey); + payload.setProperty(runtime, "ctrlKey", event.ctrlKey); + payload.setProperty(runtime, "shiftKey", event.shiftKey); + payload.setProperty(runtime, "metaKey", event.metaKey); + return payload; +}; + +void HostPlatformViewEventEmitter::onMouseEnter(const MouseEvent& mouseEvent) const { + dispatchEvent("mouseEnter", [mouseEvent](jsi::Runtime &runtime) { + return mouseEventPayload(runtime, mouseEvent); + }); +} + +void HostPlatformViewEventEmitter::onMouseLeave(const MouseEvent& mouseEvent) const { + dispatchEvent("mouseLeave", [mouseEvent](jsi::Runtime &runtime) { + return mouseEventPayload(runtime, mouseEvent); + }); +} + +#pragma mark - Drag and Drop Events + +jsi::Value HostPlatformViewEventEmitter::dataTransferPayload( + jsi::Runtime& runtime, + std::vector const& 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 = HostPlatformViewEventEmitter::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..71c405898d6fad 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,15 @@ 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; + + static jsi::Value dataTransferPayload(jsi::Runtime& runtime, std::vector const& dataTransferItems); + }; -} // 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/KeyboardEventsExample/KeyboardEventsExample.js b/packages/rn-tester/js/examples/KeyboardEventsExample/KeyboardEventsExample.js index 4726f5706b5918..ff232495940695 100644 --- a/packages/rn-tester/js/examples/KeyboardEventsExample/KeyboardEventsExample.js +++ b/packages/rn-tester/js/examples/KeyboardEventsExample/KeyboardEventsExample.js @@ -15,7 +15,6 @@ import type {KeyEvent} from 'react-native/Libraries/Types/CoreEventTypes'; import * as React from 'react'; import { - Button, Pressable, StyleSheet, Switch, @@ -128,132 +127,149 @@ function BubblingExample(): React.Node { } function KeyboardEventExample(): React.Node { - const viewRef = React.useRef | null>(null); - const [log, setLog] = React.useState>([]); + const pressableRef = React.useRef | null>(null); + const [eventLog, setEventLog] = React.useState>([]); - const clearLog = React.useCallback(() => { - setLog([]); - }, [setLog]); + function appendEvent(eventName: string, source?: string) { + const limit = 12; + setEventLog((current: Array) => { + const prefix = source != null ? `${source}: ` : ''; + return [`${prefix}${eventName}`].concat(current.slice(0, limit - 1)); + }); + } - const appendLog = React.useCallback( - (line: string) => { - const limit = 12; - const newLog = log.slice(0, limit - 1); - newLog.unshift(line); - setLog(newLog); + const handleSingleLineKeyDown = React.useCallback( + (e: KeyEvent) => { + appendEvent(`keyDown: ${e.nativeEvent.key}`, 'Single-line TextInput'); }, - [log, setLog], + [], ); - const handleKeyDown = React.useCallback( + const handleSingleLineKeyUp = React.useCallback( (e: KeyEvent) => { - appendLog('Key Down:' + e.nativeEvent.key); + appendEvent(`keyUp: ${e.nativeEvent.key}`, 'Single-line TextInput'); }, - [appendLog], + [], ); - const handleKeyUp = React.useCallback( + const handleMultiLineKeyDown = React.useCallback( (e: KeyEvent) => { - appendLog('Key Up:' + e.nativeEvent.key); + appendEvent(`keyDown: ${e.nativeEvent.key}`, 'Multi-line TextInput'); }, - [appendLog], + [], ); - const viewText = - "keyDownEvents: [{key: 'g'}, {key: 'Escape'}, {key: 'Enter'}, {key: 'ArrowLeft'}] \nkeyUpEvents: [{key: 'c'}, {key: 'd'}]"; - const viewKeyboardProps = { - onKeyDown: handleKeyDown, - keyDownEvents: [ - {key: 'g'}, - {key: 'Escape'}, - {key: 'Enter'}, - {key: 'ArrowLeft'}, - ], - onKeyUp: handleKeyUp, - keyUpEvents: [{key: 'c'}, {key: 'd'}], - }; - - const textInputText = - "keyDownEvents: [{key: 'ArrowRight'}, {key: 'ArrowDown'}, {key: 'Enter', ctrlKey: true}, \nkeyUpEvents: [{key: 'Escape'}, {key: 'Enter'}]"; - const textInputKeyboardProps = { - onKeyDown: handleKeyDown, - keyDownEvents: [ - {key: 'ArrowRight'}, - {key: 'ArrowDown'}, - {key: 'Enter', ctrlKey: true}, - ], - onKeyUp: handleKeyUp, - keyUpEvents: [{key: 'Escape'}, {key: 'Enter'}], - }; + const handleMultiLineKeyUp = React.useCallback( + (e: KeyEvent) => { + appendEvent(`keyUp: ${e.nativeEvent.key}`, 'Multi-line TextInput'); + }, + [], + ); - const textInputUnhandledText = - "keyDownEvents: [{key: 'ArrowRight'}, {key: 'ArrowDown'}, {key: 'Enter', ctrlKey: true}, \nkeyUpEvents: [{key: 'Escape'}, {key: 'Enter'}]"; - const textInputunHandledKeyboardProps = { - onKeyDown: handleKeyDown, - onKeyUp: handleKeyUp, - }; + const handlePressableKeyDown = React.useCallback( + (e: KeyEvent) => { + appendEvent(`keyDown: ${e.nativeEvent.key}`, 'Focusable Pressable'); + }, + [], + ); - React.useEffect(() => { - // Focus the first view on mount - viewRef.current?.focus(); - }, []); + const handlePressableKeyUp = React.useCallback( + (e: KeyEvent) => { + appendEvent(`keyUp: ${e.nativeEvent.key}`, 'Focusable Pressable'); + }, + [], + ); return ( - - - Key events are called when a component detects a key press.To tab - between views on macOS: Enable System Preferences / Keyboard / Shortcuts{' '} - {'>'} Use keyboard navigation to move focus between controls. + + + Examples of keyboard event handling with keyDownEvents and keyUpEvents arrays. - - {viewText} - - {textInputText} + + + + Single-line TextInput (keyDownEvents: g, Escape, Enter, ArrowLeft) + + + + + + Multi-line TextInput (keyDownEvents: ArrowRight, ArrowDown, Cmd+Enter) + - {textInputUnhandledText} - - + + + + Focusable Pressable (keyDownEvents: Space, Enter, Tab, Backspace) + + [ + styles.focusablePressable, + pressed && styles.focusablePressablePressed, + ]} focusable={true} - style={styles.input} - {...textInputunHandledKeyboardProps} - /> -