diff --git a/packages/react-native/Libraries/Blob/URL.js b/packages/react-native/Libraries/Blob/URL.js index db4bb2792a1e3a..556b8cfd008834 100644 --- a/packages/react-native/Libraries/Blob/URL.js +++ b/packages/react-native/Libraries/Blob/URL.js @@ -9,7 +9,6 @@ */ import type Blob from './Blob'; - import NativeBlobModule from './NativeBlobModule'; let BLOB_URL_PREFIX = null; @@ -18,9 +17,9 @@ if ( NativeBlobModule && typeof NativeBlobModule.getConstants().BLOB_URI_SCHEME === 'string' ) { - const constants = NativeBlobModule.getConstants(); // $FlowFixMe[incompatible-type] asserted above // $FlowFixMe[unsafe-addition] + const constants = NativeBlobModule.getConstants(); BLOB_URL_PREFIX = constants.BLOB_URI_SCHEME + ':'; if (typeof constants.BLOB_URI_HOST === 'string') { BLOB_URL_PREFIX += `//${constants.BLOB_URI_HOST}/`; @@ -51,19 +50,17 @@ if ( * * ``` */ - -export {URLSearchParams} from './URLSearchParams'; +export { URLSearchParams } from './URLSearchParams'; function validateBaseUrl(url: string) { // from this MIT-licensed gist: https://gist.github.com/dperini/729294 - return /^(?:(?:(?:https?|ftp):)?\/\/)(?:(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z0-9\u00a1-\uffff][a-z0-9\u00a1-\uffff_-]{0,62})?[a-z0-9\u00a1-\uffff]\.)*(?:[a-z\u00a1-\uffff]{2,}\.?))(?::\d{2,5})?(?:[/?#]\S*)?$/.test( - url, - ); + return /^(?:(?:(?:https?|ftp):)?\/\/)(?:(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z0-9\u00a1-\uffff][a-z0-9\u00a1-\uffff_-]{0,62})?[a-z0-9\u00a1-\uffff]\.)*(?:[a-z\u00a1-\uffff]{2,}\.?))(?::\d{2,5})?(?:[/?#]\S*)?$/.test(url); } export class URL { _url: string; _searchParamsInstance: ?URLSearchParams = null; + _parsedUrl: URL; static createObjectURL(blob: Blob): string { if (BLOB_URL_PREFIX === null) { @@ -76,7 +73,6 @@ export class URL { // Do nothing. } - // $FlowFixMe[missing-local-annot] constructor(url: string, base: string | URL) { let baseUrl = null; if (!base || validateBaseUrl(url)) { @@ -104,18 +100,21 @@ export class URL { } this._url = `${baseUrl}${url}`; } + + // Parsing the URL to use for accessors + this._parsedUrl = new globalThis.URL(this._url); } get hash(): string { - throw new Error('URL.hash is not implemented'); + return this._parsedUrl.hash; } get host(): string { - throw new Error('URL.host is not implemented'); + return this._parsedUrl.host; } get hostname(): string { - throw new Error('URL.hostname is not implemented'); + return this._parsedUrl.hostname; } get href(): string { @@ -123,32 +122,32 @@ export class URL { } get origin(): string { - throw new Error('URL.origin is not implemented'); + return this._parsedUrl.origin; } get password(): string { - throw new Error('URL.password is not implemented'); + return this._parsedUrl.password; } get pathname(): string { - throw new Error('URL.pathname not implemented'); + return this._parsedUrl.pathname; } get port(): string { - throw new Error('URL.port is not implemented'); + return this._parsedUrl.port; } get protocol(): string { - throw new Error('URL.protocol is not implemented'); + return this._parsedUrl.protocol; } get search(): string { - throw new Error('URL.search is not implemented'); + return this._parsedUrl.search; } get searchParams(): URLSearchParams { if (this._searchParamsInstance == null) { - this._searchParamsInstance = new URLSearchParams(); + this._searchParamsInstance = new URLSearchParams(this._parsedUrl.search); } return this._searchParamsInstance; } @@ -158,16 +157,12 @@ export class URL { } toString(): string { - if (this._searchParamsInstance === null) { - return this._url; - } - // $FlowFixMe[incompatible-use] - const instanceString = this._searchParamsInstance.toString(); + const instanceString = this._searchParamsInstance ? this._searchParamsInstance.toString() : ''; const separator = this._url.indexOf('?') > -1 ? '&' : '?'; - return this._url + separator + instanceString; + return this._url + (instanceString ? separator + instanceString : ''); } get username(): string { - throw new Error('URL.username is not implemented'); + return this._parsedUrl.username; } } diff --git a/packages/react-native/Libraries/Blob/__tests__/URL-test.js b/packages/react-native/Libraries/Blob/__tests__/URL-test.js index c317217c8fe724..cd21230932e1da 100644 --- a/packages/react-native/Libraries/Blob/__tests__/URL-test.js +++ b/packages/react-native/Libraries/Blob/__tests__/URL-test.js @@ -41,4 +41,20 @@ describe('URL', function () { const k = new URL('en-US/docs', 'https://developer.mozilla.org'); expect(k.href).toBe('https://developer.mozilla.org/en-US/docs'); }); + + it('should implement host, hostname, username, and password accessors correctly', () => { + const url = new URL('https://username:password@developer.mozilla.org:8080/en-US/docs?query=test#fragment'); + + // Test host + expect(url.host).toBe('developer.mozilla.org:8080'); + + // Test hostname + expect(url.hostname).toBe('developer.mozilla.org'); + + // Test username + expect(url.username).toBe('username'); + + // Test password + expect(url.password).toBe('password'); + }); }); diff --git a/packages/react-native/Libraries/Components/RefreshControl/RefreshControl.js b/packages/react-native/Libraries/Components/RefreshControl/RefreshControl.js index dbffe34c445570..b71bfc9090d245 100644 --- a/packages/react-native/Libraries/Components/RefreshControl/RefreshControl.js +++ b/packages/react-native/Libraries/Components/RefreshControl/RefreshControl.js @@ -20,8 +20,12 @@ import PullToRefreshViewNativeComponent, { const Platform = require('../../Utilities/Platform'); const React = require('react'); +const {useEffect, useRef} = React; -type IOSProps = $ReadOnly<{ +/** + * Type definitions for iOS-specific properties + */ +type IOSProps = $ReadOnly<{| /** * The color of the refresh indicator. */ @@ -36,7 +40,10 @@ type IOSProps = $ReadOnly<{ title?: ?string, }>; -type AndroidProps = $ReadOnly<{ +/** + * Type definitions for Android-specific properties + */ +type AndroidProps = $ReadOnly<{| /** * Whether the pull to refresh functionality is enabled. */ @@ -55,7 +62,10 @@ type AndroidProps = $ReadOnly<{ size?: ?('default' | 'large'), }>; -export type RefreshControlProps = $ReadOnly<{ +/** + * The main RefreshControlProps type definition + */ +export type RefreshControlProps = $ReadOnly<{| ...ViewProps, ...IOSProps, ...AndroidProps, @@ -120,6 +130,7 @@ export type RefreshControlProps = $ReadOnly<{ * * __Note:__ `refreshing` is a controlled prop, this is why it needs to be set to true * in the `onRefresh` function otherwise the refresh indicator will stop immediately. + * RefreshControl Component */ class RefreshControl extends React.Component { _nativeRef: ?React.ElementRef< @@ -175,6 +186,8 @@ class RefreshControl extends React.Component { {...props} ref={this._setNativeRef} onRefresh={this._onRefresh} + onTouchStart={this._handleTouchStart} + onTouchMove={this._handleTouchMove} /> ); } @@ -199,6 +212,29 @@ class RefreshControl extends React.Component { ) => { this._nativeRef = ref; }; + + /** + * Horizontal Gesture Handling for Android + */ + _handleTouchStart = (event) => { + if (Platform.OS === 'android' && event.nativeEvent.touches.length === 1) { + this._nativeRef?.prevTouchX = event.nativeEvent.touches[0].pageX; + } + }; + + _handleTouchMove = (event) => { + if (Platform.OS === 'android' && event.nativeEvent.touches.length === 1) { + const touchX = event.nativeEvent.touches[0].pageX; + const prevTouchX = this._nativeRef?.prevTouchX || 0; + const xDiff = Math.abs(touchX - prevTouchX); + + if (xDiff > 5 && this._nativeRef) { + AndroidSwipeRefreshLayoutCommands.cancelRefreshGesture( + this._nativeRef, + ); + } + } + }; } module.exports = RefreshControl; diff --git a/packages/react-native/React/Views/RefreshControl/RCTRefreshControl.m b/packages/react-native/React/Views/RefreshControl/RCTRefreshControl.m index 53bfd04703502d..dc7a227fe50006 100644 --- a/packages/react-native/React/Views/RefreshControl/RCTRefreshControl.m +++ b/packages/react-native/React/Views/RefreshControl/RCTRefreshControl.m @@ -221,4 +221,13 @@ - (void)refreshControlValueChanged } } +- (void)setTintColor:(UIColor *)tintColor { + if (_tintColor != tintColor) { + _tintColor = tintColor; + dispatch_async(dispatch_get_main_queue(), ^{ + self.refreshControl.tintColor = tintColor; + }); + } +} + @end diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/swiperefresh/ReactSwipeRefreshLayout.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/swiperefresh/ReactSwipeRefreshLayout.kt index e3201890f4c478..b6e255e1b80532 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/swiperefresh/ReactSwipeRefreshLayout.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/swiperefresh/ReactSwipeRefreshLayout.kt @@ -13,10 +13,11 @@ import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import com.facebook.react.bridge.ReactContext import com.facebook.react.uimanager.PixelUtil import com.facebook.react.uimanager.events.NativeGestureUtil +import kotlin.math.abs /** Basic extension of [SwipeRefreshLayout] with ReactNative-specific functionality. */ public class ReactSwipeRefreshLayout(reactContext: ReactContext) : - SwipeRefreshLayout(reactContext) { + SwipeRefreshLayout(reactContext) { private var didLayout: Boolean = false private var refreshing: Boolean = false @@ -25,6 +26,7 @@ public class ReactSwipeRefreshLayout(reactContext: ReactContext) : private var prevTouchX: Float = 0f private var intercepted: Boolean = false private var nativeGestureStarted: Boolean = false + private var isBeingDraggedHorizontally: Boolean = false public override fun setRefreshing(refreshing: Boolean) { this.refreshing = refreshing @@ -111,12 +113,17 @@ public class ReactSwipeRefreshLayout(reactContext: ReactContext) : MotionEvent.ACTION_DOWN -> { prevTouchX = ev.x intercepted = false + isBeingDraggedHorizontally = false } MotionEvent.ACTION_MOVE -> { val eventX = ev.x - val xDiff = Math.abs(eventX - prevTouchX) + val xDiff = abs(eventX - prevTouchX) - if (intercepted || xDiff > touchSlop) { + if (xDiff > touchSlop) { + isBeingDraggedHorizontally = true + } + + if (intercepted || isBeingDraggedHorizontally) { intercepted = true return false }