first commit

This commit is contained in:
2026-03-10 16:18:05 +00:00
commit 11f9c069b5
31635 changed files with 3187747 additions and 0 deletions

View File

@@ -0,0 +1,173 @@
/**
* 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
*/
import {HostInstance} from '../../../types/public/ReactNativeTypes';
import {EmitterSubscription} from '../../vendor/emitter/EventEmitter';
type AccessibilityChangeEventName =
| 'change' // deprecated, maps to screenReaderChanged
| 'boldTextChanged' // iOS-only Event
| 'grayscaleChanged' // iOS-only Event
| 'invertColorsChanged' // iOS-only Event
| 'reduceMotionChanged'
| 'highTextContrastChanged' // Android-only Event
| 'darkerSystemColorsChanged' // iOS-only Event
| 'screenReaderChanged'
| 'reduceTransparencyChanged'; // iOS-only Event
type AccessibilityChangeEvent = boolean;
type AccessibilityChangeEventHandler = (
event: AccessibilityChangeEvent,
) => void;
type AccessibilityAnnouncementEventName = 'announcementFinished'; // iOS-only Event
type AccessibilityAnnouncementFinishedEvent = {
announcement: string;
success: boolean;
};
type AccessibilityAnnouncementFinishedEventHandler = (
event: AccessibilityAnnouncementFinishedEvent,
) => void;
type AccessibilityEventTypes = 'click' | 'focus' | 'viewHoverEnter';
/**
* @see https://reactnative.dev/docs/accessibilityinfo
*/
export interface AccessibilityInfoStatic {
/**
* Query whether bold text is currently enabled.
*
* @platform ios
*/
isBoldTextEnabled: () => Promise<boolean>;
/**
* Query whether grayscale is currently enabled.
*
* @platform ios
*/
isGrayscaleEnabled: () => Promise<boolean>;
/**
* Query whether invert colors is currently enabled.
*
* @platform ios
*/
isInvertColorsEnabled: () => Promise<boolean>;
/**
* Query whether reduce motion is currently enabled.
*/
isReduceMotionEnabled: () => Promise<boolean>;
/**
*
* Query whether high text contrast is currently enabled.
*
* @platform android
*/
isHighTextContrastEnabled: () => Promise<boolean>;
/**
* Query whether darker system colors is currently enabled.
*
* @platform ios
*/
isDarkerSystemColorsEnabled: () => Promise<boolean>;
/**
* Query whether reduce motion and prefer cross-fade transitions settings are currently enabled.
*
* Returns a promise which resolves to a boolean.
* The result is `true` when prefer cross-fade transitions is enabled and `false` otherwise.
*/
prefersCrossFadeTransitions(): Promise<boolean>;
/**
* Query whether reduce transparency is currently enabled.
*
* @platform ios
*/
isReduceTransparencyEnabled: () => Promise<boolean>;
/**
* Query whether a screen reader is currently enabled.
*/
isScreenReaderEnabled: () => Promise<boolean>;
/**
* Query whether Accessibility Service is currently enabled.
*
* Returns a promise which resolves to a boolean.
* The result is `true` when any service is enabled and `false` otherwise.
*
* @platform android
*/
isAccessibilityServiceEnabled(): Promise<boolean>;
/**
* Add an event handler. Supported events:
* - announcementFinished: iOS-only event. Fires when the screen reader has finished making an announcement.
* The argument to the event handler is a dictionary with these keys:
* - announcement: The string announced by the screen reader.
* - success: A boolean indicating whether the announcement was successfully made.
* - AccessibilityEventName constants other than announcementFinished: Fires on accessibility feature change.
* The argument to the event handler is a boolean.
* The boolean is true when the related event's feature is enabled and false otherwise.
*
*/
addEventListener(
eventName: AccessibilityChangeEventName,
handler: AccessibilityChangeEventHandler,
): EmitterSubscription;
addEventListener(
eventName: AccessibilityAnnouncementEventName,
handler: AccessibilityAnnouncementFinishedEventHandler,
): EmitterSubscription;
/**
* Set accessibility focus to a react component.
*/
setAccessibilityFocus: (reactTag: number) => void;
/**
* Post a string to be announced by the screen reader.
*/
announceForAccessibility: (announcement: string) => void;
/**
* Post a string to be announced by the screen reader.
* - `announcement`: The string announced by the screen reader.
* - `options`: An object that configures the reading options.
* - `queue`: The announcement will be queued behind existing announcements. iOS only.
*/
announceForAccessibilityWithOptions(
announcement: string,
options: {queue?: boolean | undefined},
): void;
/**
* Gets the timeout in millisecond that the user needs.
* This value is set in "Time to take action (Accessibility timeout)" of "Accessibility" settings.
*
* @platform android
*/
getRecommendedTimeoutMillis: (originalTimeout: number) => Promise<number>;
sendAccessibilityEvent: (
handle: HostInstance,
eventType: AccessibilityEventTypes,
) => void;
}
export const AccessibilityInfo: AccessibilityInfoStatic;
export type AccessibilityInfo = AccessibilityInfoStatic;

View File

@@ -0,0 +1,521 @@
/**
* 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.
*
* @flow strict-local
* @format
*/
import type {HostInstance} from '../../../src/private/types/HostInstance';
import type {EventSubscription} from '../../vendor/emitter/EventEmitter';
import RCTDeviceEventEmitter from '../../EventEmitter/RCTDeviceEventEmitter';
import {sendAccessibilityEvent} from '../../ReactNative/RendererProxy';
import Platform from '../../Utilities/Platform';
import legacySendAccessibilityEvent from './legacySendAccessibilityEvent';
import NativeAccessibilityInfoAndroid from './NativeAccessibilityInfo';
import NativeAccessibilityManagerIOS from './NativeAccessibilityManager';
// Events that are only supported on Android.
type AccessibilityEventDefinitionsAndroid = {
accessibilityServiceChanged: [boolean],
highTextContrastChanged: [boolean],
};
// Events that are only supported on iOS.
type AccessibilityEventDefinitionsIOS = {
announcementFinished: [{announcement: string, success: boolean}],
boldTextChanged: [boolean],
grayscaleChanged: [boolean],
invertColorsChanged: [boolean],
reduceTransparencyChanged: [boolean],
darkerSystemColorsChanged: [boolean],
};
type AccessibilityEventDefinitions = {
...AccessibilityEventDefinitionsAndroid,
...AccessibilityEventDefinitionsIOS,
change: [boolean], // screenReaderChanged
reduceMotionChanged: [boolean],
screenReaderChanged: [boolean],
};
type AccessibilityEventTypes = 'click' | 'focus' | 'viewHoverEnter';
// Mapping of public event names to platform-specific event names.
const EventNames: Map<
$Keys<AccessibilityEventDefinitions>,
string,
> = Platform.OS === 'android'
? new Map([
['change', 'touchExplorationDidChange'],
['reduceMotionChanged', 'reduceMotionDidChange'],
['highTextContrastChanged', 'highTextContrastDidChange'],
['screenReaderChanged', 'touchExplorationDidChange'],
['accessibilityServiceChanged', 'accessibilityServiceDidChange'],
['invertColorsChanged', 'invertColorDidChange'],
['grayscaleChanged', 'grayscaleModeDidChange'],
])
: new Map([
['announcementFinished', 'announcementFinished'],
['boldTextChanged', 'boldTextChanged'],
['change', 'screenReaderChanged'],
['grayscaleChanged', 'grayscaleChanged'],
['invertColorsChanged', 'invertColorsChanged'],
['reduceMotionChanged', 'reduceMotionChanged'],
['reduceTransparencyChanged', 'reduceTransparencyChanged'],
['screenReaderChanged', 'screenReaderChanged'],
['darkerSystemColorsChanged', 'darkerSystemColorsChanged'],
]);
/**
* Sometimes it's useful to know whether or not the device has a screen reader
* that is currently active. The `AccessibilityInfo` API is designed for this
* purpose. You can use it to query the current state of the screen reader as
* well as to register to be notified when the state of the screen reader
* changes.
*
* See https://reactnative.dev/docs/accessibilityinfo
*/
const AccessibilityInfo = {
/**
* Query whether bold text is currently enabled.
*
* Returns a promise which resolves to a boolean.
* The result is `true` when bold text is enabled and `false` otherwise.
*
* See https://reactnative.dev/docs/accessibilityinfo#isBoldTextEnabled
*/
isBoldTextEnabled(): Promise<boolean> {
if (Platform.OS === 'android') {
return Promise.resolve(false);
} else {
return new Promise((resolve, reject) => {
if (NativeAccessibilityManagerIOS != null) {
NativeAccessibilityManagerIOS.getCurrentBoldTextState(
resolve,
reject,
);
} else {
reject(new Error('NativeAccessibilityManagerIOS is not available'));
}
});
}
},
/**
* Query whether grayscale is currently enabled.
*
* Returns a promise which resolves to a boolean.
* The result is `true` when grayscale is enabled and `false` otherwise.
*
* See https://reactnative.dev/docs/accessibilityinfo#isGrayscaleEnabled
*/
isGrayscaleEnabled(): Promise<boolean> {
if (Platform.OS === 'android') {
return new Promise((resolve, reject) => {
if (NativeAccessibilityInfoAndroid?.isGrayscaleEnabled != null) {
NativeAccessibilityInfoAndroid.isGrayscaleEnabled(resolve);
} else {
reject(
new Error(
'NativeAccessibilityInfoAndroid.isGrayscaleEnabled is not available',
),
);
}
});
} else {
return new Promise((resolve, reject) => {
if (NativeAccessibilityManagerIOS != null) {
NativeAccessibilityManagerIOS.getCurrentGrayscaleState(
resolve,
reject,
);
} else {
reject(new Error('AccessibilityInfo native module is not available'));
}
});
}
},
/**
* Query whether inverted colors are currently enabled.
*
* Returns a promise which resolves to a boolean.
* The result is `true` when invert color is enabled and `false` otherwise.
*
* See https://reactnative.dev/docs/accessibilityinfo#isInvertColorsEnabled
*/
isInvertColorsEnabled(): Promise<boolean> {
if (Platform.OS === 'android') {
return new Promise((resolve, reject) => {
if (NativeAccessibilityInfoAndroid?.isInvertColorsEnabled != null) {
NativeAccessibilityInfoAndroid.isInvertColorsEnabled(resolve);
} else {
reject(
new Error(
'NativeAccessibilityInfoAndroid.isInvertColorsEnabled is not available',
),
);
}
});
} else {
return new Promise((resolve, reject) => {
if (NativeAccessibilityManagerIOS != null) {
NativeAccessibilityManagerIOS.getCurrentInvertColorsState(
resolve,
reject,
);
} else {
reject(new Error('AccessibilityInfo native module is not available'));
}
});
}
},
/**
* Query whether reduced motion is currently enabled.
*
* Returns a promise which resolves to a boolean.
* The result is `true` when a reduce motion is enabled and `false` otherwise.
*
* See https://reactnative.dev/docs/accessibilityinfo#isReduceMotionEnabled
*/
isReduceMotionEnabled(): Promise<boolean> {
return new Promise((resolve, reject) => {
if (Platform.OS === 'android') {
if (NativeAccessibilityInfoAndroid != null) {
NativeAccessibilityInfoAndroid.isReduceMotionEnabled(resolve);
} else {
reject(new Error('AccessibilityInfo native module is not available'));
}
} else {
if (NativeAccessibilityManagerIOS != null) {
NativeAccessibilityManagerIOS.getCurrentReduceMotionState(
resolve,
reject,
);
} else {
reject(new Error('NativeAccessibilityManagerIOS is not available'));
}
}
});
},
/**
* Query whether high text contrast is currently enabled. Android only.
*
* Returns a promise which resolves to a boolean.
* The result is `true` when high text contrast is enabled and `false` otherwise.
*/
isHighTextContrastEnabled(): Promise<boolean> {
return new Promise((resolve, reject) => {
if (Platform.OS === 'android') {
if (NativeAccessibilityInfoAndroid?.isHighTextContrastEnabled != null) {
NativeAccessibilityInfoAndroid.isHighTextContrastEnabled(resolve);
} else {
reject(
new Error(
'NativeAccessibilityInfoAndroid.isHighTextContrastEnabled is not available',
),
);
}
} else {
return Promise.resolve(false);
}
});
},
/**
* Query whether dark system colors is currently enabled. iOS only.
*
* Returns a promise which resolves to a boolean.
* The result is `true` when dark system colors is enabled and `false` otherwise.
*/
isDarkerSystemColorsEnabled(): Promise<boolean> {
return new Promise((resolve, reject) => {
if (Platform.OS === 'android') {
return Promise.resolve(false);
} else {
if (
NativeAccessibilityManagerIOS?.getCurrentDarkerSystemColorsState !=
null
) {
NativeAccessibilityManagerIOS.getCurrentDarkerSystemColorsState(
resolve,
reject,
);
} else {
reject(
new Error(
'NativeAccessibilityManagerIOS.getCurrentDarkerSystemColorsState is not available',
),
);
}
}
});
},
/**
* Query whether reduce motion and prefer cross-fade transitions settings are currently enabled.
*
* Returns a promise which resolves to a boolean.
* The result is `true` when prefer cross-fade transitions is enabled and `false` otherwise.
*
* See https://reactnative.dev/docs/accessibilityinfo#prefersCrossFadeTransitions
*/
prefersCrossFadeTransitions(): Promise<boolean> {
return new Promise((resolve, reject) => {
if (Platform.OS === 'android') {
return Promise.resolve(false);
} else {
if (
NativeAccessibilityManagerIOS?.getCurrentPrefersCrossFadeTransitionsState !=
null
) {
NativeAccessibilityManagerIOS.getCurrentPrefersCrossFadeTransitionsState(
resolve,
reject,
);
} else {
reject(
new Error(
'NativeAccessibilityManagerIOS.getCurrentPrefersCrossFadeTransitionsState is not available',
),
);
}
}
});
},
/**
* Query whether reduced transparency is currently enabled.
*
* Returns a promise which resolves to a boolean.
* The result is `true` when a reduce transparency is enabled and `false` otherwise.
*
* See https://reactnative.dev/docs/accessibilityinfo#isReduceTransparencyEnabled
*/
isReduceTransparencyEnabled(): Promise<boolean> {
if (Platform.OS === 'android') {
return Promise.resolve(false);
} else {
return new Promise((resolve, reject) => {
if (NativeAccessibilityManagerIOS != null) {
NativeAccessibilityManagerIOS.getCurrentReduceTransparencyState(
resolve,
reject,
);
} else {
reject(new Error('NativeAccessibilityManagerIOS is not available'));
}
});
}
},
/**
* Query whether a screen reader is currently enabled.
*
* Returns a promise which resolves to a boolean.
* The result is `true` when a screen reader is enabled and `false` otherwise.
*
* See https://reactnative.dev/docs/accessibilityinfo#isScreenReaderEnabled
*/
isScreenReaderEnabled(): Promise<boolean> {
return new Promise((resolve, reject) => {
if (Platform.OS === 'android') {
if (NativeAccessibilityInfoAndroid != null) {
NativeAccessibilityInfoAndroid.isTouchExplorationEnabled(resolve);
} else {
reject(new Error('NativeAccessibilityInfoAndroid is not available'));
}
} else {
if (NativeAccessibilityManagerIOS != null) {
NativeAccessibilityManagerIOS.getCurrentVoiceOverState(
resolve,
reject,
);
} else {
reject(new Error('NativeAccessibilityManagerIOS is not available'));
}
}
});
},
/**
* Query whether Accessibility Service is currently enabled.
*
* Returns a promise which resolves to a boolean.
* The result is `true` when any service is enabled and `false` otherwise.
*
* @platform android
*
* See https://reactnative.dev/docs/accessibilityinfo/#isaccessibilityserviceenabled-android
*/
isAccessibilityServiceEnabled(): Promise<boolean> {
return new Promise((resolve, reject) => {
if (Platform.OS === 'android') {
if (
NativeAccessibilityInfoAndroid != null &&
NativeAccessibilityInfoAndroid.isAccessibilityServiceEnabled != null
) {
NativeAccessibilityInfoAndroid.isAccessibilityServiceEnabled(resolve);
} else {
reject(
new Error(
'NativeAccessibilityInfoAndroid.isAccessibilityServiceEnabled is not available',
),
);
}
} else {
reject(
new Error(
'isAccessibilityServiceEnabled is only available on Android',
),
);
}
});
},
/**
* Add an event handler. Supported events:
*
* - `reduceMotionChanged`: Fires when the state of the reduce motion toggle changes.
* The argument to the event handler is a boolean. The boolean is `true` when a reduce
* motion is enabled (or when "Transition Animation Scale" in "Developer options" is
* "Animation off") and `false` otherwise.
* - `screenReaderChanged`: Fires when the state of the screen reader changes. The argument
* to the event handler is a boolean. The boolean is `true` when a screen
* reader is enabled and `false` otherwise.
*
* These events are only supported on iOS:
*
* - `boldTextChanged`: iOS-only event. Fires when the state of the bold text toggle changes.
* The argument to the event handler is a boolean. The boolean is `true` when a bold text
* is enabled and `false` otherwise.
* - `grayscaleChanged`: iOS-only event. Fires when the state of the gray scale toggle changes.
* The argument to the event handler is a boolean. The boolean is `true` when a gray scale
* is enabled and `false` otherwise.
* - `invertColorsChanged`: iOS-only event. Fires when the state of the invert colors toggle
* changes. The argument to the event handler is a boolean. The boolean is `true` when a invert
* colors is enabled and `false` otherwise.
* - `reduceTransparencyChanged`: iOS-only event. Fires when the state of the reduce transparency
* toggle changes. The argument to the event handler is a boolean. The boolean is `true`
* when a reduce transparency is enabled and `false` otherwise.
* - `announcementFinished`: iOS-only event. Fires when the screen reader has
* finished making an announcement. The argument to the event handler is a
* dictionary with these keys:
* - `announcement`: The string announced by the screen reader.
* - `success`: A boolean indicating whether the announcement was
* successfully made.
* - `darkerSystemColorsChanged`: iOS-only event. Fires when the state of the dark system colors
* toggle changes. The argument to the event handler is a boolean. The boolean is `true` when
* dark system colors is enabled and `false` otherwise.
*
* These events are only supported on Android:
*
* - `highTextContrastChanged`: Android-only event. Fires when the state of the high text contrast
* toggle changes. The argument to the event handler is a boolean. The boolean is `true` when
* high text contrast is enabled and `false` otherwise.
*
* See https://reactnative.dev/docs/accessibilityinfo#addeventlistener
*/
addEventListener<K: $Keys<AccessibilityEventDefinitions>>(
eventName: K,
// $FlowFixMe[incompatible-type] - Flow bug with unions and generics (T128099423)
handler: (...AccessibilityEventDefinitions[K]) => void,
): EventSubscription {
const deviceEventName = EventNames.get(eventName);
return deviceEventName == null
? {remove(): void {}}
: // $FlowFixMe[incompatible-type]
RCTDeviceEventEmitter.addListener(deviceEventName, handler);
},
/**
* Set accessibility focus to a React component.
*
* See https://reactnative.dev/docs/accessibilityinfo#setaccessibilityfocus
*/
setAccessibilityFocus(reactTag: number): void {
legacySendAccessibilityEvent(reactTag, 'focus');
},
/**
* Send a named accessibility event to a HostComponent.
*/
sendAccessibilityEvent(
handle: HostInstance,
eventType: AccessibilityEventTypes,
) {
// iOS only supports 'focus' event types
if (Platform.OS === 'ios' && eventType === 'click') {
return;
}
// route through React renderer to distinguish between Fabric and non-Fabric handles
sendAccessibilityEvent(handle, eventType);
},
/**
* Post a string to be announced by the screen reader.
*
* See https://reactnative.dev/docs/accessibilityinfo#announceforaccessibility
*/
announceForAccessibility(announcement: string): void {
if (Platform.OS === 'android') {
NativeAccessibilityInfoAndroid?.announceForAccessibility(announcement);
} else {
NativeAccessibilityManagerIOS?.announceForAccessibility(announcement);
}
},
/**
* Post a string to be announced by the screen reader.
* - `announcement`: The string announced by the screen reader.
* - `options`: An object that configures the reading options.
* - `queue`: The announcement will be queued behind existing announcements. iOS only.
*/
announceForAccessibilityWithOptions(
announcement: string,
options: {queue?: boolean},
): void {
if (Platform.OS === 'android') {
NativeAccessibilityInfoAndroid?.announceForAccessibility(announcement);
} else {
if (NativeAccessibilityManagerIOS?.announceForAccessibilityWithOptions) {
NativeAccessibilityManagerIOS?.announceForAccessibilityWithOptions(
announcement,
options,
);
} else {
NativeAccessibilityManagerIOS?.announceForAccessibility(announcement);
}
}
},
/**
* Get the recommended timeout for changes to the UI needed by this user.
*
* See https://reactnative.dev/docs/accessibilityinfo#getrecommendedtimeoutmillis
*/
getRecommendedTimeoutMillis(originalTimeout: number): Promise<number> {
if (Platform.OS === 'android') {
return new Promise((resolve, reject) => {
if (NativeAccessibilityInfoAndroid?.getRecommendedTimeoutMillis) {
NativeAccessibilityInfoAndroid.getRecommendedTimeoutMillis(
originalTimeout,
resolve,
);
} else {
resolve(originalTimeout);
}
});
} else {
return Promise.resolve(originalTimeout);
}
},
};
export default AccessibilityInfo;

View File

@@ -0,0 +1,12 @@
/**
* 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.
*
* @flow strict
* @format
*/
export * from '../../../src/private/specs_DEPRECATED/modules/NativeAccessibilityInfo';
export {default} from '../../../src/private/specs_DEPRECATED/modules/NativeAccessibilityInfo';

View File

@@ -0,0 +1,12 @@
/**
* 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.
*
* @flow strict-local
* @format
*/
export * from '../../../src/private/specs_DEPRECATED/modules/NativeAccessibilityManager';
export {default} from '../../../src/private/specs_DEPRECATED/modules/NativeAccessibilityManager';

View File

@@ -0,0 +1,36 @@
/**
* 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.
*
* @flow strict-local
* @format
*/
import UIManager from '../../ReactNative/UIManager';
import nullthrows from 'nullthrows';
/**
* This is a function exposed to the React Renderer that can be used by the
* pre-Fabric renderer to emit accessibility events to pre-Fabric nodes.
*/
function legacySendAccessibilityEvent(
reactTag: number,
eventType: string,
): void {
if (eventType === 'focus') {
nullthrows(UIManager.sendAccessibilityEvent)(
reactTag,
UIManager.getConstants().AccessibilityEventTypes.typeViewFocused,
);
}
if (eventType === 'click') {
nullthrows(UIManager.sendAccessibilityEvent)(
reactTag,
UIManager.getConstants().AccessibilityEventTypes.typeViewClicked,
);
}
}
export default legacySendAccessibilityEvent;

View File

@@ -0,0 +1,26 @@
/**
* 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.
*
* @flow strict-local
* @format
*/
import NativeAccessibilityManager from './NativeAccessibilityManager';
/**
* This is a function exposed to the React Renderer that can be used by the
* pre-Fabric renderer to emit accessibility events to pre-Fabric nodes.
*/
function legacySendAccessibilityEvent(
reactTag: number,
eventType: string,
): void {
if (eventType === 'focus' && NativeAccessibilityManager) {
NativeAccessibilityManager.setAccessibilityFocus(reactTag);
}
}
export default legacySendAccessibilityEvent;

View File

@@ -0,0 +1,17 @@
/**
* 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.
*
* @flow strict-local
* @format
*/
// NOTE: This file supports backwards compatibility of subpath (deep) imports
// from 'react-native' with platform-specific extensions. It can be deleted
// once we remove the "./*" mapping from package.json "exports".
import legacySendAccessibilityEvent from './legacySendAccessibilityEvent';
export default legacySendAccessibilityEvent;

View File

@@ -0,0 +1,20 @@
/**
* 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.
*
* @flow strict-local
* @format
*/
/**
* This is a function exposed to the React Renderer that can be used by the
* pre-Fabric renderer to emit accessibility events to pre-Fabric nodes.
*/
declare function legacySendAccessibilityEvent(
reactTag: number,
eventType: string,
): void;
export default legacySendAccessibilityEvent;

View File

@@ -0,0 +1,86 @@
/**
* 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
*/
import type * as React from 'react';
import {Constructor} from '../../../types/private/Utilities';
import {HostInstance} from '../../../types/public/ReactNativeTypes';
import {ColorValue, StyleProp} from '../../StyleSheet/StyleSheet';
import {ViewStyle} from '../../StyleSheet/StyleSheetTypes';
import {LayoutChangeEvent} from '../../Types/CoreEventTypes';
import {ViewProps} from '../View/ViewPropTypes';
/**
* @see https://reactnative.dev/docs/activityindicator#props
*/
export interface ActivityIndicatorProps extends ViewProps {
/**
* Whether to show the indicator (true, the default) or hide it (false).
*/
animating?: boolean | undefined;
/**
* The foreground color of the spinner (default is gray).
*/
color?: ColorValue | undefined;
/**
* Whether the indicator should hide when not animating (true by default).
*/
hidesWhenStopped?: boolean | undefined;
/**
* Size of the indicator.
* Small has a height of 20, large has a height of 36.
*
* enum('small', 'large')
*/
size?: number | 'small' | 'large' | undefined;
style?: StyleProp<ViewStyle> | undefined;
}
declare class ActivityIndicatorComponent extends React.Component<ActivityIndicatorProps> {}
declare const ActivityIndicatorBase: Constructor<HostInstance> &
typeof ActivityIndicatorComponent;
export class ActivityIndicator extends ActivityIndicatorBase {}
/**
* @see https://reactnative.dev/docs/activityindicatorios#props
*/
export interface ActivityIndicatorIOSProps extends ViewProps {
/**
* Whether to show the indicator (true, the default) or hide it (false).
*/
animating?: boolean | undefined;
/**
* The foreground color of the spinner (default is gray).
*/
color?: ColorValue | undefined;
/**
* Whether the indicator should hide when not animating (true by default).
*/
hidesWhenStopped?: boolean | undefined;
/**
* Invoked on mount and layout changes with
*/
onLayout?: ((event: LayoutChangeEvent) => void) | undefined;
/**
* Size of the indicator.
* Small has a height of 20, large has a height of 36.
*
* enum('small', 'large')
*/
size?: 'small' | 'large' | undefined;
style?: StyleProp<ViewStyle> | undefined;
}

View File

@@ -0,0 +1,178 @@
/**
* 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.
*
* @flow
* @format
*/
'use strict';
import type {HostComponent} from '../../../src/private/types/HostComponent';
import type {ViewProps} from '../View/ViewPropTypes';
import StyleSheet, {type ColorValue} from '../../StyleSheet/StyleSheet';
import Platform from '../../Utilities/Platform';
import View from '../View/View';
import * as React from 'react';
const PlatformActivityIndicator =
Platform.OS === 'android'
? require('../ProgressBarAndroid/ProgressBarAndroid').default
: require('./ActivityIndicatorViewNativeComponent').default;
const GRAY = '#999999';
type IndicatorSize = number | 'small' | 'large';
type ActivityIndicatorIOSProps = $ReadOnly<{
/**
Whether the indicator should hide when not animating.
@platform ios
*/
hidesWhenStopped?: ?boolean,
}>;
export type ActivityIndicatorProps = $ReadOnly<{
...ViewProps,
...ActivityIndicatorIOSProps,
/**
Whether to show the indicator (`true`) or hide it (`false`).
*/
animating?: ?boolean,
/**
The foreground color of the spinner.
@default {@platform android} `null` (system accent default color)
@default {@platform ios} '#999999'
*/
color?: ?ColorValue,
/**
Size of the indicator.
@type enum(`'small'`, `'large'`)
@type {@platform android} number
*/
size?: ?IndicatorSize,
}>;
const ActivityIndicator: component(
ref?: React.RefSetter<HostComponent<empty>>,
...props: ActivityIndicatorProps
) = ({
ref: forwardedRef,
animating = true,
color = Platform.OS === 'ios' ? GRAY : null,
hidesWhenStopped = true,
onLayout,
size = 'small',
style,
...restProps
}: {
ref?: any,
...ActivityIndicatorProps,
}) => {
let sizeStyle;
let sizeProp;
switch (size) {
case 'small':
sizeStyle = styles.sizeSmall;
sizeProp = 'small';
break;
case 'large':
sizeStyle = styles.sizeLarge;
sizeProp = 'large';
break;
default:
sizeStyle = {height: size, width: size};
break;
}
const nativeProps = {
animating,
color,
hidesWhenStopped,
...restProps,
ref: forwardedRef,
style: sizeStyle,
size: sizeProp,
};
const androidProps = {
styleAttr: 'Normal',
indeterminate: true,
};
return (
<View
onLayout={onLayout}
style={StyleSheet.compose(styles.container, style)}>
{Platform.OS === 'android' ? (
// $FlowFixMe[prop-missing] Flow doesn't know when this is the android component
// $FlowFixMe[incompatible-type]
<PlatformActivityIndicator {...nativeProps} {...androidProps} />
) : (
/* $FlowFixMe[incompatible-type] (>=0.106.0 site=react_native_android_fb) This comment
* suppresses an error found when Flow v0.106 was deployed. To see the
* error, delete this comment and run Flow. */
<PlatformActivityIndicator {...nativeProps} />
)}
</View>
);
};
/**
Displays a circular loading indicator.
```SnackPlayer name=ActivityIndicator%20Example
import React from 'react';
import {ActivityIndicator, StyleSheet, View} from 'react-native';
const App = () => (
<View style={[styles.container, styles.horizontal]}>
<ActivityIndicator />
<ActivityIndicator size="large" />
<ActivityIndicator size="small" color="#0000ff" />
<ActivityIndicator size="large" color="#00ff00" />
</View>
);
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
},
horizontal: {
flexDirection: 'row',
justifyContent: 'space-around',
padding: 10,
},
});
export default App;
```
*/
ActivityIndicator.displayName = 'ActivityIndicator';
const styles = StyleSheet.create({
container: {
alignItems: 'center',
justifyContent: 'center',
},
sizeSmall: {
width: 20,
height: 20,
},
sizeLarge: {
width: 36,
height: 36,
},
});
export default ActivityIndicator;

View File

@@ -0,0 +1,12 @@
/**
* 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.
*
* @flow strict-local
* @format
*/
export * from '../../../src/private/specs_DEPRECATED/components/ActivityIndicatorViewNativeComponent';
export {default} from '../../../src/private/specs_DEPRECATED/components/ActivityIndicatorViewNativeComponent';

View File

@@ -0,0 +1,42 @@
/**
* 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
*/
import type * as React from 'react';
import {ColorValue} from '../StyleSheet/StyleSheet';
import {TouchableNativeFeedbackProps} from './Touchable/TouchableNativeFeedback';
import {TouchableOpacityProps} from './Touchable/TouchableOpacity';
export interface ButtonProps
extends Pick<
TouchableNativeFeedbackProps & TouchableOpacityProps,
| 'accessibilityLabel'
| 'accessibilityState'
| 'hasTVPreferredFocus'
| 'nextFocusDown'
| 'nextFocusForward'
| 'nextFocusLeft'
| 'nextFocusRight'
| 'nextFocusUp'
| 'testID'
| 'disabled'
| 'onPress'
| 'touchSoundDisabled'
> {
/**
* Text to display inside the button. On Android the given title will be converted to the uppercased form.
*/
title: string;
/**
* Color of the text (iOS), or background color of the button (Android).
*/
color?: ColorValue | undefined;
}
export class Button extends React.Component<ButtonProps> {}

View File

@@ -0,0 +1,444 @@
/**
* 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.
*
* @flow
* @format
*/
'use strict';
import type {TextStyleProp, ViewStyleProp} from '../StyleSheet/StyleSheet';
import type {GestureResponderEvent} from '../Types/CoreEventTypes';
import type {
AccessibilityActionEvent,
AccessibilityActionInfo,
AccessibilityState,
} from './View/ViewAccessibility';
import StyleSheet, {type ColorValue} from '../StyleSheet/StyleSheet';
import Text from '../Text/Text';
import Platform from '../Utilities/Platform';
import TouchableNativeFeedback from './Touchable/TouchableNativeFeedback';
import TouchableOpacity from './Touchable/TouchableOpacity';
import View from './View/View';
import invariant from 'invariant';
import * as React from 'react';
export type ButtonProps = $ReadOnly<{
/**
Text to display inside the button. On Android the given title will be
converted to the uppercased form.
*/
title: string,
/**
Handler to be called when the user taps the button. The first function
argument is an event in form of [GestureResponderEvent](pressevent).
*/
onPress?: (event?: GestureResponderEvent) => mixed,
/**
If `true`, doesn't play system sound on touch.
@platform android
@default false
*/
touchSoundDisabled?: ?boolean,
/**
Color of the text (iOS), or background color of the button (Android).
@default {@platform android} '#2196F3'
@default {@platform ios} '#007AFF'
*/
color?: ?ColorValue,
/**
TV preferred focus.
@platform tv
@default false
@deprecated Use `focusable` instead
*/
hasTVPreferredFocus?: ?boolean,
/**
Designates the next view to receive focus when the user navigates down. See
the [Android documentation][android:nextFocusDown].
[android:nextFocusDown]:
https://developer.android.com/reference/android/view/View.html#attr_android:nextFocusDown
@platform android, tv
*/
nextFocusDown?: ?number,
/**
Designates the next view to receive focus when the user navigates forward.
See the [Android documentation][android:nextFocusForward].
[android:nextFocusForward]:
https://developer.android.com/reference/android/view/View.html#attr_android:nextFocusForward
@platform android, tv
*/
nextFocusForward?: ?number,
/**
Designates the next view to receive focus when the user navigates left. See
the [Android documentation][android:nextFocusLeft].
[android:nextFocusLeft]:
https://developer.android.com/reference/android/view/View.html#attr_android:nextFocusLeft
@platform android, tv
*/
nextFocusLeft?: ?number,
/**
Designates the next view to receive focus when the user navigates right. See
the [Android documentation][android:nextFocusRight].
[android:nextFocusRight]:
https://developer.android.com/reference/android/view/View.html#attr_android:nextFocusRight
@platform android, tv
*/
nextFocusRight?: ?number,
/**
Designates the next view to receive focus when the user navigates up. See
the [Android documentation][android:nextFocusUp].
[android:nextFocusUp]:
https://developer.android.com/reference/android/view/View.html#attr_android:nextFocusUp
@platform android, tv
*/
nextFocusUp?: ?number,
/**
Text to display for blindness accessibility features.
*/
accessibilityLabel?: ?string,
/**
* Alias for accessibilityLabel https://reactnative.dev/docs/view#accessibilitylabel
* https://github.com/facebook/react-native/issues/34424
*/
'aria-label'?: ?string,
/**
If `true`, disable all interactions for this component.
@default false
*/
disabled?: ?boolean,
/**
Used to locate this view in end-to-end tests.
*/
testID?: ?string,
/**
* Accessibility props.
*/
accessible?: ?boolean,
accessibilityActions?: ?$ReadOnlyArray<AccessibilityActionInfo>,
onAccessibilityAction?: ?(event: AccessibilityActionEvent) => mixed,
accessibilityState?: ?AccessibilityState,
/**
* alias for accessibilityState
*
* see https://reactnative.dev/docs/accessibility#accessibilitystate
*/
'aria-busy'?: ?boolean,
'aria-checked'?: ?boolean | 'mixed',
'aria-disabled'?: ?boolean,
'aria-expanded'?: ?boolean,
'aria-selected'?: ?boolean,
/**
* [Android] Controlling if a view fires accessibility events and if it is reported to accessibility services.
*/
importantForAccessibility?: ?('auto' | 'yes' | 'no' | 'no-hide-descendants'),
accessibilityHint?: ?string,
accessibilityLanguage?: ?Stringish,
}>;
/**
A basic button component that should render nicely on any platform. Supports a
minimal level of customization.
If this button doesn't look right for your app, you can build your own button
using [TouchableOpacity](touchableopacity) or
[TouchableWithoutFeedback](touchablewithoutfeedback). For inspiration, look at
the [source code for this button component][button:source]. Or, take a look at
the [wide variety of button components built by the community]
[button:examples].
[button:source]:
https://github.com/facebook/react-native/blob/HEAD/Libraries/Components/Button.js
[button:examples]:
https://js.coach/?menu%5Bcollections%5D=React%20Native&page=1&query=button
```jsx
<Button
onPress={onPressLearnMore}
title="Learn More"
color="#841584"
accessibilityLabel="Learn more about this purple button"
/>
```
```SnackPlayer name=Button%20Example
import React from 'react';
import { StyleSheet, Button, View, SafeAreaView, Text, Alert } from 'react-native';
const Separator = () => (
<View style={styles.separator} />
);
const App = () => (
<SafeAreaView style={styles.container}>
<View>
<Text style={styles.title}>
The title and onPress handler are required. It is recommended to set accessibilityLabel to help make your app usable by everyone.
</Text>
<Button
title="Press me"
onPress={() => Alert.alert('Simple Button pressed')}
/>
</View>
<Separator />
<View>
<Text style={styles.title}>
Adjust the color in a way that looks standard on each platform. On iOS, the color prop controls the color of the text. On Android, the color adjusts the background color of the button.
</Text>
<Button
title="Press me"
color="#f194ff"
onPress={() => Alert.alert('Button with adjusted color pressed')}
/>
</View>
<Separator />
<View>
<Text style={styles.title}>
All interaction for the component are disabled.
</Text>
<Button
title="Press me"
disabled
onPress={() => Alert.alert('Cannot press this one')}
/>
</View>
<Separator />
<View>
<Text style={styles.title}>
This layout strategy lets the title define the width of the button.
</Text>
<View style={styles.fixToText}>
<Button
title="Left button"
onPress={() => Alert.alert('Left button pressed')}
/>
<Button
title="Right button"
onPress={() => Alert.alert('Right button pressed')}
/>
</View>
</View>
</SafeAreaView>
);
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
marginHorizontal: 16,
},
title: {
textAlign: 'center',
marginVertical: 8,
},
fixToText: {
flexDirection: 'row',
justifyContent: 'space-between',
},
separator: {
marginVertical: 8,
borderBottomColor: '#737373',
borderBottomWidth: StyleSheet.hairlineWidth,
},
});
export default App;
```
*/
const NativeTouchable:
| typeof TouchableNativeFeedback
| typeof TouchableOpacity =
Platform.OS === 'android' ? TouchableNativeFeedback : TouchableOpacity;
type ButtonRef = React.ElementRef<typeof NativeTouchable>;
const Button: component(
ref?: React.RefSetter<ButtonRef>,
...props: ButtonProps
) = ({ref, ...props}: {ref?: React.RefSetter<ButtonRef>, ...ButtonProps}) => {
const {
accessibilityLabel,
accessibilityState,
'aria-busy': ariaBusy,
'aria-checked': ariaChecked,
'aria-disabled': ariaDisabled,
'aria-expanded': ariaExpanded,
'aria-label': ariaLabel,
'aria-selected': ariaSelected,
importantForAccessibility,
color,
onPress,
touchSoundDisabled,
title,
hasTVPreferredFocus,
nextFocusDown,
nextFocusForward,
nextFocusLeft,
nextFocusRight,
nextFocusUp,
testID,
accessible,
accessibilityActions,
accessibilityHint,
accessibilityLanguage,
onAccessibilityAction,
} = props;
const buttonStyles: Array<ViewStyleProp> = [styles.button];
const textStyles: Array<TextStyleProp> = [styles.text];
if (color) {
if (Platform.OS === 'ios') {
textStyles.push({color: color});
} else {
buttonStyles.push({backgroundColor: color});
}
}
let _accessibilityState = {
busy: ariaBusy ?? accessibilityState?.busy,
checked: ariaChecked ?? accessibilityState?.checked,
disabled: ariaDisabled ?? accessibilityState?.disabled,
expanded: ariaExpanded ?? accessibilityState?.expanded,
selected: ariaSelected ?? accessibilityState?.selected,
};
const disabled =
props.disabled != null ? props.disabled : _accessibilityState?.disabled;
_accessibilityState =
disabled !== _accessibilityState?.disabled
? {..._accessibilityState, disabled}
: _accessibilityState;
if (disabled) {
buttonStyles.push(styles.buttonDisabled);
textStyles.push(styles.textDisabled);
}
invariant(
typeof title === 'string',
'The title prop of a Button must be a string',
);
const formattedTitle =
Platform.OS === 'android' ? title.toUpperCase() : title;
// If `no` is specified for `importantForAccessibility`, it will be changed to `no-hide-descendants` because the text inside should not be focused.
const _importantForAccessibility =
importantForAccessibility === 'no'
? 'no-hide-descendants'
: importantForAccessibility;
return (
<NativeTouchable
accessible={accessible}
accessibilityActions={accessibilityActions}
onAccessibilityAction={onAccessibilityAction}
accessibilityLabel={ariaLabel || accessibilityLabel}
accessibilityHint={accessibilityHint}
accessibilityLanguage={accessibilityLanguage}
accessibilityRole="button"
accessibilityState={_accessibilityState}
importantForAccessibility={_importantForAccessibility}
hasTVPreferredFocus={hasTVPreferredFocus}
nextFocusDown={nextFocusDown}
nextFocusForward={nextFocusForward}
nextFocusLeft={nextFocusLeft}
nextFocusRight={nextFocusRight}
nextFocusUp={nextFocusUp}
testID={testID}
disabled={disabled}
onPress={onPress}
touchSoundDisabled={touchSoundDisabled}
// $FlowFixMe[incompatible-exact]
// $FlowFixMe[prop-missing]
// $FlowFixMe[incompatible-type]
ref={ref}>
<View style={buttonStyles}>
<Text style={textStyles} disabled={disabled}>
{formattedTitle}
</Text>
</View>
</NativeTouchable>
);
};
Button.displayName = 'Button';
const styles = StyleSheet.create({
button: Platform.select({
ios: {},
android: {
elevation: 4,
// Material design blue from https://material.google.com/style/color.html#color-color-palette
backgroundColor: '#2196F3',
borderRadius: 2,
},
}),
text: {
textAlign: 'center',
margin: 8,
...Platform.select({
ios: {
// iOS blue from https://developer.apple.com/ios/human-interface-guidelines/visual-design/color/
color: '#007AFF',
fontSize: 18,
},
android: {
color: 'white',
fontWeight: '500',
},
}),
},
buttonDisabled: Platform.select({
ios: {},
android: {
elevation: 0,
backgroundColor: '#dfdfdf',
},
}),
textDisabled: Platform.select({
ios: {
color: '#cdcdcd',
},
android: {
color: '#a1a1a1',
},
}),
});
export default Button;

View File

@@ -0,0 +1,28 @@
/**
* 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
*/
export interface ClipboardStatic {
getString(): Promise<string>;
setString(content: string): void;
}
/**
* Clipboard has been extracted from react-native core and will be removed in a future release.
* It can now be installed and imported from `@react-native-clipboard/clipboard` instead of 'react-native'.
* @see https://github.com/react-native-clipboard/clipboard
* @deprecated
*/
export const Clipboard: ClipboardStatic;
/**
* Clipboard has been extracted from react-native core and will be removed in a future release.
* It can now be installed and imported from `@react-native-clipboard/clipboard` instead of 'react-native'.
* @see https://github.com/react-native-clipboard/clipboard
* @deprecated
*/
export type Clipboard = ClipboardStatic;

View File

@@ -0,0 +1,40 @@
/**
* 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.
*
* @flow strict
* @format
*/
import NativeClipboard from './NativeClipboard';
/**
* `Clipboard` gives you an interface for setting and getting content from Clipboard on both iOS and Android
*/
export default {
/**
* Get content of string type, this method returns a `Promise`, so you can use following code to get clipboard content
* ```javascript
* async _getContent() {
* var content = await Clipboard.getString();
* }
* ```
*/
getString(): Promise<string> {
return NativeClipboard.getString();
},
/**
* Set content of string type. You can use following code to set clipboard content
* ```javascript
* _setContent() {
* Clipboard.setString('hello world');
* }
* ```
* @param {string} content the content to be stored in the clipboard.
*/
setString(content: string) {
NativeClipboard.setString(content);
},
};

View File

@@ -0,0 +1,12 @@
/**
* 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.
*
* @flow strict
* @format
*/
export * from '../../../src/private/specs_DEPRECATED/modules/NativeClipboard';
export {default} from '../../../src/private/specs_DEPRECATED/modules/NativeClipboard';

View File

@@ -0,0 +1,12 @@
/**
* 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.
*
* @flow strict-local
* @format
*/
export * from '../../../src/private/specs_DEPRECATED/components/AndroidDrawerLayoutNativeComponent';
export {default} from '../../../src/private/specs_DEPRECATED/components/AndroidDrawerLayoutNativeComponent';

View File

@@ -0,0 +1,305 @@
/**
* 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.
*
* @flow
* @format
*/
import type {
MeasureInWindowOnSuccessCallback,
MeasureLayoutOnSuccessCallback,
MeasureOnSuccessCallback,
} from '../../../src/private/types/HostInstance';
import type {
DrawerLayoutAndroidMethods,
DrawerLayoutAndroidProps,
DrawerLayoutAndroidState,
} from './DrawerLayoutAndroidTypes';
import StyleSheet from '../../StyleSheet/StyleSheet';
import dismissKeyboard from '../../Utilities/dismissKeyboard';
import StatusBar from '../StatusBar/StatusBar';
import View from '../View/View';
import AndroidDrawerLayoutNativeComponent, {
Commands,
} from './AndroidDrawerLayoutNativeComponent';
import nullthrows from 'nullthrows';
import * as React from 'react';
import {createRef} from 'react';
const DRAWER_STATES = ['Idle', 'Dragging', 'Settling'] as const;
/**
* React component that wraps the platform `DrawerLayout` (Android only). The
* Drawer (typically used for navigation) is rendered with `renderNavigationView`
* and direct children are the main view (where your content goes). The navigation
* view is initially not visible on the screen, but can be pulled in from the
* side of the window specified by the `drawerPosition` prop and its width can
* be set by the `drawerWidth` prop.
*
* Example:
*
* ```
* render: function() {
* var navigationView = (
* <View style={{flex: 1, backgroundColor: '#fff'}}>
* <Text style={{margin: 10, fontSize: 15, textAlign: 'left'}}>I'm in the Drawer!</Text>
* </View>
* );
* return (
* <DrawerLayoutAndroid
* drawerWidth={300}
* drawerPosition="left"
* renderNavigationView={() => navigationView}>
* <View style={{flex: 1, alignItems: 'center'}}>
* <Text style={{margin: 10, fontSize: 15, textAlign: 'right'}}>Hello</Text>
* <Text style={{margin: 10, fontSize: 15, textAlign: 'right'}}>World!</Text>
* </View>
* </DrawerLayoutAndroid>
* );
* },
* ```
*/
class DrawerLayoutAndroid
extends React.Component<DrawerLayoutAndroidProps, DrawerLayoutAndroidState>
implements DrawerLayoutAndroidMethods
{
static get positions(): mixed {
console.warn(
'Setting DrawerLayoutAndroid drawerPosition using `DrawerLayoutAndroid.positions` is deprecated. Instead pass the string value "left" or "right"',
);
return {Left: 'left', Right: 'right'};
}
// $FlowFixMe[missing-local-annot]
_nativeRef =
createRef<React.ElementRef<typeof AndroidDrawerLayoutNativeComponent>>();
state: DrawerLayoutAndroidState = {
drawerOpened: false,
};
render(): React.Node {
const {
drawerBackgroundColor = 'white',
onDrawerStateChanged,
renderNavigationView,
onDrawerOpen,
onDrawerClose,
...props
} = this.props;
const drawStatusBar = this.props.statusBarBackgroundColor != null;
const drawerViewWrapper = (
<View
style={[
styles.drawerSubview,
{
width: this.props.drawerWidth,
backgroundColor: drawerBackgroundColor,
},
]}
pointerEvents={this.state.drawerOpened ? 'auto' : 'none'}
collapsable={false}>
{renderNavigationView()}
{drawStatusBar && <View style={styles.drawerStatusBar} />}
</View>
);
const childrenWrapper = (
<View style={styles.mainSubview} collapsable={false}>
{drawStatusBar && (
<StatusBar
translucent
backgroundColor={this.props.statusBarBackgroundColor}
/>
)}
{drawStatusBar && (
<View
style={[
styles.statusBar,
{backgroundColor: this.props.statusBarBackgroundColor},
]}
/>
)}
{this.props.children}
</View>
);
return (
<AndroidDrawerLayoutNativeComponent
{...props}
ref={this._nativeRef}
drawerBackgroundColor={drawerBackgroundColor}
drawerWidth={this.props.drawerWidth}
drawerPosition={this.props.drawerPosition}
drawerLockMode={this.props.drawerLockMode}
style={[styles.base, this.props.style]}
onDrawerSlide={this._onDrawerSlide}
onDrawerOpen={this._onDrawerOpen}
onDrawerClose={this._onDrawerClose}
onDrawerStateChanged={this._onDrawerStateChanged}>
{childrenWrapper}
{drawerViewWrapper}
</AndroidDrawerLayoutNativeComponent>
);
}
/* $FlowFixMe[missing-local-annot] The type annotation(s) required by Flow's
* LTI update could not be added via codemod */
_onDrawerSlide = event => {
if (this.props.onDrawerSlide) {
// $FlowFixMe[unused-promise]
this.props.onDrawerSlide(event);
}
if (this.props.keyboardDismissMode === 'on-drag') {
dismissKeyboard();
}
};
_onDrawerOpen = () => {
this.setState({
drawerOpened: true,
});
if (this.props.onDrawerOpen) {
this.props.onDrawerOpen();
}
};
_onDrawerClose = () => {
this.setState({
drawerOpened: false,
});
if (this.props.onDrawerClose) {
this.props.onDrawerClose();
}
};
/* $FlowFixMe[missing-local-annot] The type annotation(s) required by Flow's
* LTI update could not be added via codemod */
_onDrawerStateChanged = event => {
if (this.props.onDrawerStateChanged) {
this.props.onDrawerStateChanged(
DRAWER_STATES[event.nativeEvent.drawerState],
);
}
};
/**
* Opens the drawer.
*/
openDrawer() {
Commands.openDrawer(nullthrows(this._nativeRef.current));
}
/**
* Closes the drawer.
*/
closeDrawer() {
Commands.closeDrawer(nullthrows(this._nativeRef.current));
}
/**
* Closing and opening example
* Note: To access the drawer you have to give it a ref
*
* Class component:
*
* render () {
* this.openDrawer = () => {
* this.refs.DRAWER.openDrawer()
* }
* this.closeDrawer = () => {
* this.refs.DRAWER.closeDrawer()
* }
* return (
* <DrawerLayoutAndroid ref={'DRAWER'}>
* {children}
* </DrawerLayoutAndroid>
* )
* }
*
* Function component:
*
* const drawerRef = useRef()
* const openDrawer = () => {
* drawerRef.current.openDrawer()
* }
* const closeDrawer = () => {
* drawerRef.current.closeDrawer()
* }
* return (
* <DrawerLayoutAndroid ref={drawerRef}>
* {children}
* </DrawerLayoutAndroid>
* )
*/
/**
* Native methods
*/
blur() {
nullthrows(this._nativeRef.current).blur();
}
focus() {
nullthrows(this._nativeRef.current).focus();
}
measure(callback: MeasureOnSuccessCallback) {
nullthrows(this._nativeRef.current).measure(callback);
}
measureInWindow(callback: MeasureInWindowOnSuccessCallback) {
nullthrows(this._nativeRef.current).measureInWindow(callback);
}
measureLayout(
relativeToNativeNode: number,
onSuccess: MeasureLayoutOnSuccessCallback,
onFail?: () => void,
) {
nullthrows(this._nativeRef.current).measureLayout(
relativeToNativeNode,
onSuccess,
onFail,
);
}
setNativeProps(nativeProps: Object) {
nullthrows(this._nativeRef.current).setNativeProps(nativeProps);
}
}
const styles = StyleSheet.create({
base: {
flex: 1,
elevation: 16,
},
mainSubview: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
},
drawerSubview: {
position: 'absolute',
top: 0,
bottom: 0,
},
statusBar: {
height: StatusBar.currentHeight,
},
drawerStatusBar: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: StatusBar.currentHeight,
backgroundColor: 'rgba(0, 0, 0, 0.251)',
},
});
export default DrawerLayoutAndroid as $FlowFixMe;

View File

@@ -0,0 +1,141 @@
/**
* 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
*/
import type * as React from 'react';
import {Constructor} from '../../../types/private/Utilities';
import {HostInstance} from '../../../types/public/ReactNativeTypes';
import {ColorValue} from '../../StyleSheet/StyleSheet';
import {
NativeSyntheticEvent,
NativeTouchEvent,
} from '../../Types/CoreEventTypes';
import {ViewProps} from '../View/ViewPropTypes';
export interface DrawerSlideEvent
extends NativeSyntheticEvent<NativeTouchEvent> {}
/**
* @see DrawerLayoutAndroid.android.js
*/
export interface DrawerLayoutAndroidProps extends ViewProps {
/**
* Specifies the background color of the drawer. The default value
* is white. If you want to set the opacity of the drawer, use rgba.
* Example:
* return (
* <DrawerLayoutAndroid drawerBackgroundColor="rgba(0,0,0,0.5)">
* </DrawerLayoutAndroid>
*);
*/
drawerBackgroundColor?: ColorValue | undefined;
/**
* Specifies the lock mode of the drawer. The drawer can be locked
* in 3 states:
*
* - unlocked (default), meaning that the drawer will respond
* (open/close) to touch gestures.
*
* - locked-closed, meaning that the drawer will stay closed and not
* respond to gestures.
*
* - locked-open, meaning that the drawer will stay opened and
* not respond to gestures. The drawer may still be opened and
* closed programmatically (openDrawer/closeDrawer).
*/
drawerLockMode?: 'unlocked' | 'locked-closed' | 'locked-open' | undefined;
/**
* Specifies the side of the screen from which the drawer will slide in.
* - 'left' (the default)
* - 'right'
*/
drawerPosition?: 'left' | 'right' | undefined;
/**
* Specifies the width of the drawer, more precisely the width of the
* view that be pulled in from the edge of the window.
*/
drawerWidth?: number | undefined;
/**
* Determines whether the keyboard gets dismissed in response to a drag.
* - 'none' (the default), drags do not dismiss the keyboard.
* - 'on-drag', the keyboard is dismissed when a drag begins.
*/
keyboardDismissMode?: 'none' | 'on-drag' | undefined;
/**
* Function called whenever the navigation view has been closed.
*/
onDrawerClose?: (() => void) | undefined;
/**
* Function called whenever the navigation view has been opened.
*/
onDrawerOpen?: (() => void) | undefined;
/**
* Function called whenever there is an interaction with the navigation view.
*/
onDrawerSlide?: ((event: DrawerSlideEvent) => void) | undefined;
/**
* Function called when the drawer state has changed.
* The drawer can be in 3 states:
* - idle, meaning there is no interaction with the navigation
* view happening at the time
* - dragging, meaning there is currently an interaction with the
* navigation view
* - settling, meaning that there was an interaction with the
* navigation view, and the navigation view is now finishing
* it's closing or opening animation
*/
onDrawerStateChanged?:
| ((event: 'Idle' | 'Dragging' | 'Settling') => void)
| undefined;
/**
* The navigation view that will be rendered to the side of the
* screen and can be pulled in.
*/
renderNavigationView: () => React.JSX.Element;
/**
* Make the drawer take the entire screen and draw the background of
* the status bar to allow it to open over the status bar. It will
* only have an effect on API 21+.
*/
statusBarBackgroundColor?: ColorValue | undefined;
}
interface DrawerPosition {
Left: number;
Right: number;
}
declare class DrawerLayoutAndroidComponent extends React.Component<DrawerLayoutAndroidProps> {}
declare const DrawerLayoutAndroidBase: Constructor<HostInstance> &
typeof DrawerLayoutAndroidComponent;
export class DrawerLayoutAndroid extends DrawerLayoutAndroidBase {
/**
* drawer's positions.
*/
positions: DrawerPosition;
/**
* Opens the drawer.
*/
openDrawer(): void;
/**
* Closes the drawer.
*/
closeDrawer(): void;
}

View File

@@ -0,0 +1,15 @@
/**
* 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.
*
* @flow strict-local
* @format
*/
'use strict';
import DrawerLayoutAndroidFallback from './DrawerLayoutAndroidFallback';
export default DrawerLayoutAndroidFallback;

View File

@@ -0,0 +1,17 @@
/**
* 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.
*
* @flow
* @format
*/
// NOTE: This file supports backwards compatibility of subpath (deep) imports
// from 'react-native' with platform-specific extensions. It can be deleted
// once we remove the "./*" mapping from package.json "exports".
import DrawerLayoutAndroid from './DrawerLayoutAndroid';
export default DrawerLayoutAndroid;

View File

@@ -0,0 +1,18 @@
/**
* 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.
*
* @flow strict-local
* @format
*/
import DrawerLayoutAndroid from './DrawerLayoutAndroidFallback';
export type {
DrawerLayoutAndroidProps,
DrawerSlideEvent,
} from './DrawerLayoutAndroidTypes';
export default DrawerLayoutAndroid;

View File

@@ -0,0 +1,71 @@
/**
* 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.
*
* @flow strict-local
* @format
*/
'use strict';
import type {
MeasureInWindowOnSuccessCallback,
MeasureLayoutOnSuccessCallback,
MeasureOnSuccessCallback,
} from '../../../src/private/types/HostInstance';
import type {
DrawerLayoutAndroidMethods,
DrawerLayoutAndroidProps,
DrawerLayoutAndroidState,
} from './DrawerLayoutAndroidTypes';
import UnimplementedView from '../UnimplementedViews/UnimplementedView';
import * as React from 'react';
export default class DrawerLayoutAndroid
extends React.Component<DrawerLayoutAndroidProps, DrawerLayoutAndroidState>
implements DrawerLayoutAndroidMethods
{
render(): React.Node {
return <UnimplementedView {...this.props} />;
}
openDrawer(): void {
throw new Error('DrawerLayoutAndroid is only available on Android');
}
closeDrawer(): void {
throw new Error('DrawerLayoutAndroid is only available on Android');
}
blur(): void {
throw new Error('DrawerLayoutAndroid is only available on Android');
}
focus(): void {
throw new Error('DrawerLayoutAndroid is only available on Android');
}
measure(callback: MeasureOnSuccessCallback): void {
throw new Error('DrawerLayoutAndroid is only available on Android');
}
measureInWindow(callback: MeasureInWindowOnSuccessCallback): void {
throw new Error('DrawerLayoutAndroid is only available on Android');
}
measureLayout(
relativeToNativeNode: number,
onSuccess: MeasureLayoutOnSuccessCallback,
onFail?: () => void,
): void {
throw new Error('DrawerLayoutAndroid is only available on Android');
}
// $FlowFixMe[unclear-type]
setNativeProps(nativeProps: Object): void {
throw new Error('DrawerLayoutAndroid is only available on Android');
}
}

View File

@@ -0,0 +1,138 @@
/**
* 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.
*
* @flow
* @format
*/
import type {
MeasureInWindowOnSuccessCallback,
MeasureLayoutOnSuccessCallback,
MeasureOnSuccessCallback,
} from '../../../src/private/types/HostInstance';
import type {ColorValue} from '../../StyleSheet/StyleSheet';
import type {NativeSyntheticEvent} from '../../Types/CoreEventTypes';
import type {ViewProps} from '../View/ViewPropTypes';
import * as React from 'react';
export type DrawerStates = 'Idle' | 'Dragging' | 'Settling';
export type DrawerSlideEvent = NativeSyntheticEvent<
$ReadOnly<{
offset: number,
}>,
>;
export type DrawerLayoutAndroidProps = $ReadOnly<{
...ViewProps,
/**
* Determines whether the keyboard gets dismissed in response to a drag.
* - 'none' (the default), drags do not dismiss the keyboard.
* - 'on-drag', the keyboard is dismissed when a drag begins.
*/
keyboardDismissMode?: ?('none' | 'on-drag'),
/**
* Specifies the background color of the drawer. The default value is white.
* If you want to set the opacity of the drawer, use rgba. Example:
*
* ```
* return (
* <DrawerLayoutAndroid drawerBackgroundColor="rgba(0,0,0,0.5)">
* </DrawerLayoutAndroid>
* );
* ```
*/
drawerBackgroundColor?: ?ColorValue,
/**
* Specifies the side of the screen from which the drawer will slide in.
*/
drawerPosition: ?('left' | 'right'),
/**
* Specifies the width of the drawer, more precisely the width of the view that be pulled in
* from the edge of the window.
*/
drawerWidth?: ?number,
/**
* Specifies the lock mode of the drawer. The drawer can be locked in 3 states:
* - unlocked (default), meaning that the drawer will respond (open/close) to touch gestures.
* - locked-closed, meaning that the drawer will stay closed and not respond to gestures.
* - locked-open, meaning that the drawer will stay opened and not respond to gestures.
* The drawer may still be opened and closed programmatically (`openDrawer`/`closeDrawer`).
*/
drawerLockMode?: ?('unlocked' | 'locked-closed' | 'locked-open'),
/**
* Function called whenever there is an interaction with the navigation view.
*/
onDrawerSlide?: ?(event: DrawerSlideEvent) => mixed,
/**
* Function called when the drawer state has changed. The drawer can be in 3 states:
* - Idle, meaning there is no interaction with the navigation view happening at the time
* - Dragging, meaning there is currently an interaction with the navigation view
* - Settling, meaning that there was an interaction with the navigation view, and the
* navigation view is now finishing its closing or opening animation
*/
onDrawerStateChanged?: ?(state: DrawerStates) => mixed,
/**
* Function called whenever the navigation view has been opened.
*/
onDrawerOpen?: ?() => mixed,
/**
* Function called whenever the navigation view has been closed.
*/
onDrawerClose?: ?() => mixed,
/**
* The navigation view that will be rendered to the side of the screen and can be pulled in.
*/
renderNavigationView: () => React.MixedElement,
/**
* Make the drawer take the entire screen and draw the background of the
* status bar to allow it to open over the status bar. It will only have an
* effect on API 21+.
*/
statusBarBackgroundColor?: ?ColorValue,
}>;
export type DrawerLayoutAndroidState = {
drawerOpened: boolean,
};
export interface DrawerLayoutAndroidMethods {
/**
* Opens the drawer.
*/
openDrawer(): void;
/**
* Closes the drawer.
*/
closeDrawer(): void;
/**
* Native methods
*/
blur(): void;
focus(): void;
measure(callback: MeasureOnSuccessCallback): void;
measureInWindow(callback: MeasureInWindowOnSuccessCallback): void;
measureLayout(
relativeToNativeNode: number,
onSuccess: MeasureLayoutOnSuccessCallback,
onFail?: () => void,
): void;
setNativeProps(nativeProps: Object): void;
}

View File

@@ -0,0 +1,109 @@
/**
* 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
*/
import {NativeEventEmitter} from '../../EventEmitter/NativeEventEmitter';
import {EmitterSubscription} from '../../vendor/emitter/EventEmitter';
export type KeyboardEventName =
| 'keyboardWillShow'
| 'keyboardDidShow'
| 'keyboardWillHide'
| 'keyboardDidHide'
| 'keyboardWillChangeFrame'
| 'keyboardDidChangeFrame';
export type KeyboardEventEasing =
| 'easeIn'
| 'easeInEaseOut'
| 'easeOut'
| 'linear'
| 'keyboard';
type KeyboardMetrics = {
screenX: number;
screenY: number;
width: number;
height: number;
};
interface KeyboardEventIOS {
/**
* @platform ios
*/
startCoordinates: KeyboardMetrics;
/**
* @platform ios
*/
isEventFromThisApp: boolean;
}
export interface KeyboardEvent extends Partial<KeyboardEventIOS> {
/**
* Always set to 0 on Android.
*/
duration: number;
/**
* Always set to "keyboard" on Android.
*/
easing: KeyboardEventEasing;
endCoordinates: KeyboardMetrics;
}
type KeyboardEventListener = (event: KeyboardEvent) => void;
export interface KeyboardStatic extends NativeEventEmitter {
/**
* Dismisses the active keyboard and removes focus.
*/
dismiss: () => void;
/**
* The `addListener` function connects a JavaScript function to an identified native
* keyboard notification event.
*
* This function then returns the reference to the listener.
*
* {string} eventName The `nativeEvent` is the string that identifies the event you're listening for. This
*can be any of the following:
*
* - `keyboardWillShow`
* - `keyboardDidShow`
* - `keyboardWillHide`
* - `keyboardDidHide`
* - `keyboardWillChangeFrame`
* - `keyboardDidChangeFrame`
*
* Note that if you set `android:windowSoftInputMode` to `adjustResize` or `adjustNothing`,
* only `keyboardDidShow` and `keyboardDidHide` events will be available on Android.
* `keyboardWillShow` as well as `keyboardWillHide` are generally not available on Android
* since there is no native corresponding event.
*
* {function} callback function to be called when the event fires.
*/
addListener: (
eventType: KeyboardEventName,
listener: KeyboardEventListener,
) => EmitterSubscription;
/**
* Useful for syncing TextInput (or other keyboard accessory view) size of
* position changes with keyboard movements.
*/
scheduleLayoutAnimation: (event: KeyboardEvent) => void;
/**
* Whether the keyboard is last known to be visible.
*/
isVisible(): boolean;
/**
* Return the metrics of the soft-keyboard if visible.
*/
metrics(): KeyboardMetrics | undefined;
}
export const Keyboard: KeyboardStatic;

View File

@@ -0,0 +1,207 @@
/**
* 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.
*
* @flow strict-local
* @format
*/
import type {EventSubscription} from '../../vendor/emitter/EventEmitter';
import NativeEventEmitter from '../../EventEmitter/NativeEventEmitter';
import LayoutAnimation from '../../LayoutAnimation/LayoutAnimation';
import dismissKeyboard from '../../Utilities/dismissKeyboard';
import Platform from '../../Utilities/Platform';
import NativeKeyboardObserver from './NativeKeyboardObserver';
export type KeyboardEventName = $Keys<KeyboardEventDefinitions>;
export type KeyboardEventEasing =
| 'easeIn'
| 'easeInEaseOut'
| 'easeOut'
| 'linear'
| 'keyboard';
export type KeyboardMetrics = $ReadOnly<{
screenX: number,
screenY: number,
width: number,
height: number,
}>;
export type KeyboardEvent = AndroidKeyboardEvent | IOSKeyboardEvent;
type BaseKeyboardEvent = {
duration: number,
easing: KeyboardEventEasing,
endCoordinates: KeyboardMetrics,
};
export type AndroidKeyboardEvent = $ReadOnly<{
...BaseKeyboardEvent,
duration: 0,
easing: 'keyboard',
}>;
export type IOSKeyboardEvent = $ReadOnly<{
...BaseKeyboardEvent,
startCoordinates: KeyboardMetrics,
isEventFromThisApp: boolean,
}>;
type KeyboardEventDefinitions = {
keyboardWillShow: [KeyboardEvent],
keyboardDidShow: [KeyboardEvent],
keyboardWillHide: [KeyboardEvent],
keyboardDidHide: [KeyboardEvent],
keyboardWillChangeFrame: [KeyboardEvent],
keyboardDidChangeFrame: [KeyboardEvent],
};
/**
* `Keyboard` module to control keyboard events.
*
* ### Usage
*
* The Keyboard module allows you to listen for native events and react to them, as
* well as make changes to the keyboard, like dismissing it.
*
*```
* import React, { Component } from 'react';
* import { Keyboard, TextInput } from 'react-native';
*
* class Example extends Component {
* componentWillMount () {
* this.keyboardDidShowListener = Keyboard.addListener('keyboardDidShow', this._keyboardDidShow);
* this.keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', this._keyboardDidHide);
* }
*
* componentWillUnmount () {
* this.keyboardDidShowListener.remove();
* this.keyboardDidHideListener.remove();
* }
*
* _keyboardDidShow () {
* alert('Keyboard Shown');
* }
*
* _keyboardDidHide () {
* alert('Keyboard Hidden');
* }
*
* render() {
* return (
* <TextInput
* onSubmitEditing={Keyboard.dismiss}
* />
* );
* }
* }
*```
*/
class KeyboardImpl {
_currentlyShowing: ?KeyboardEvent;
_emitter: NativeEventEmitter<KeyboardEventDefinitions> =
new NativeEventEmitter(
// T88715063: NativeEventEmitter only used this parameter on iOS. Now it uses it on all platforms, so this code was modified automatically to preserve its behavior
// If you want to use the native module on other platforms, please remove this condition and test its behavior
Platform.OS !== 'ios' ? null : NativeKeyboardObserver,
);
constructor() {
this.addListener('keyboardDidShow', ev => {
this._currentlyShowing = ev;
});
this.addListener('keyboardDidHide', _ev => {
this._currentlyShowing = null;
});
}
/**
* The `addListener` function connects a JavaScript function to an identified native
* keyboard notification event.
*
* This function then returns the reference to the listener.
*
* @param {string} eventName The `nativeEvent` is the string that identifies the event you're listening for. This
*can be any of the following:
*
* - `keyboardWillShow`
* - `keyboardDidShow`
* - `keyboardWillHide`
* - `keyboardDidHide`
* - `keyboardWillChangeFrame`
* - `keyboardDidChangeFrame`
*
* Android versions prior to API 30 rely on observing layout changes when
* `android:windowSoftInputMode` is set to `adjustResize` or `adjustPan`.
*
* `keyboardWillShow` as well as `keyboardWillHide` are not available on Android since there is
* no native corresponding event.
*
* @param {function} callback function to be called when the event fires.
*/
addListener<K: $Keys<KeyboardEventDefinitions>>(
eventType: K,
listener: (...KeyboardEventDefinitions[K]) => mixed,
context?: mixed,
): EventSubscription {
return this._emitter.addListener(eventType, listener);
}
/**
* Removes all listeners for a specific event type.
*
* @param {string} eventType The native event string listeners are watching which will be removed.
*/
removeAllListeners<K: $Keys<KeyboardEventDefinitions>>(eventType: ?K): void {
this._emitter.removeAllListeners(eventType);
}
/**
* Dismisses the active keyboard and removes focus.
*/
dismiss(): void {
dismissKeyboard();
}
/**
* Whether the keyboard is last known to be visible.
*/
isVisible(): boolean {
return !!this._currentlyShowing;
}
/**
* Return the metrics of the soft-keyboard if visible.
*/
metrics(): ?KeyboardMetrics {
return this._currentlyShowing?.endCoordinates;
}
/**
* Useful for syncing TextInput (or other keyboard accessory view) size of
* position changes with keyboard movements.
*/
scheduleLayoutAnimation(event: KeyboardEvent): void {
const {duration, easing} = event;
if (duration != null && duration !== 0) {
LayoutAnimation.configureNext({
duration: duration,
update: {
duration: duration,
type: (easing != null && LayoutAnimation.Types[easing]) || 'keyboard',
},
});
}
}
}
const Keyboard: KeyboardImpl = new KeyboardImpl();
export default Keyboard;

View File

@@ -0,0 +1,46 @@
/**
* 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
*/
import type * as React from 'react';
import {Constructor} from '../../../types/private/Utilities';
import {TimerMixin} from '../../../types/private/TimerMixin';
import {StyleProp} from '../../StyleSheet/StyleSheet';
import {ViewStyle} from '../../StyleSheet/StyleSheetTypes';
import {ViewProps} from '../View/ViewPropTypes';
/**
* It is a component to solve the common problem of views that need to move out of the way of the virtual keyboard.
* It can automatically adjust either its position or bottom padding based on the position of the keyboard.
*/
declare class KeyboardAvoidingViewComponent extends React.Component<KeyboardAvoidingViewProps> {}
declare const KeyboardAvoidingViewBase: Constructor<TimerMixin> &
typeof KeyboardAvoidingViewComponent;
export class KeyboardAvoidingView extends KeyboardAvoidingViewBase {}
export interface KeyboardAvoidingViewProps extends ViewProps {
behavior?: 'height' | 'position' | 'padding' | undefined;
/**
* The style of the content container(View) when behavior is 'position'.
*/
contentContainerStyle?: StyleProp<ViewStyle> | undefined;
/**
* This is the distance between the top of the user screen and the react native view,
* may be non-zero in some use cases.
*/
keyboardVerticalOffset?: number | undefined;
/**
* Enables or disables the KeyboardAvoidingView.
*
* Default is true
*/
enabled?: boolean | undefined;
}

View File

@@ -0,0 +1,300 @@
/**
* 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.
*
* @flow strict-local
* @format
*/
import type {ViewStyleProp} from '../../StyleSheet/StyleSheet';
import type {
ViewLayout,
ViewLayoutEvent,
ViewProps,
} from '../View/ViewPropTypes';
import type {KeyboardEvent, KeyboardMetrics} from './Keyboard';
import LayoutAnimation from '../../LayoutAnimation/LayoutAnimation';
import StyleSheet from '../../StyleSheet/StyleSheet';
import Platform from '../../Utilities/Platform';
import {type EventSubscription} from '../../vendor/emitter/EventEmitter';
import AccessibilityInfo from '../AccessibilityInfo/AccessibilityInfo';
import View from '../View/View';
import Keyboard from './Keyboard';
import * as React from 'react';
import {createRef} from 'react';
export type KeyboardAvoidingViewProps = $ReadOnly<{
...ViewProps,
/**
* Specify how to react to the presence of the keyboard.
*/
behavior?: ?('height' | 'position' | 'padding'),
/**
* Style of the content container when `behavior` is 'position'.
*/
contentContainerStyle?: ?ViewStyleProp,
/**
* Controls whether this `KeyboardAvoidingView` instance should take effect.
* This is useful when more than one is on the screen. Defaults to true.
*/
enabled?: ?boolean,
/**
* Distance between the top of the user screen and the React Native view. This
* may be non-zero in some cases. Defaults to 0.
*/
keyboardVerticalOffset?: number,
}>;
type KeyboardAvoidingViewState = {
bottom: number,
};
/**
* View that moves out of the way when the keyboard appears by automatically
* adjusting its height, position, or bottom padding.
*/
class KeyboardAvoidingView extends React.Component<
KeyboardAvoidingViewProps,
KeyboardAvoidingViewState,
> {
_frame: ?ViewLayout = null;
_keyboardEvent: ?KeyboardEvent = null;
_subscriptions: Array<EventSubscription> = [];
viewRef: {current: React.ElementRef<typeof View> | null, ...};
_initialFrameHeight: number = 0;
_bottom: number = 0;
constructor(props: KeyboardAvoidingViewProps) {
super(props);
this.state = {bottom: 0};
this.viewRef = createRef();
}
async _relativeKeyboardHeight(
keyboardFrame: KeyboardMetrics,
): Promise<number> {
const frame = this._frame;
if (!frame || !keyboardFrame) {
return 0;
}
// On iOS when Prefer Cross-Fade Transitions is enabled, the keyboard position
// & height is reported differently (0 instead of Y position value matching height of frame)
if (
Platform.OS === 'ios' &&
keyboardFrame.screenY === 0 &&
(await AccessibilityInfo.prefersCrossFadeTransitions())
) {
return 0;
}
const keyboardY =
keyboardFrame.screenY - (this.props.keyboardVerticalOffset ?? 0);
if (this.props.behavior === 'height') {
return Math.max(
this.state.bottom + frame.y + frame.height - keyboardY,
0,
);
}
// Calculate the displacement needed for the view such that it
// no longer overlaps with the keyboard
return Math.max(frame.y + frame.height - keyboardY, 0);
}
_onKeyboardChange = (event: ?KeyboardEvent) => {
this._keyboardEvent = event;
// $FlowFixMe[unused-promise]
this._updateBottomIfNecessary();
};
_onKeyboardHide = (event: ?KeyboardEvent) => {
this._keyboardEvent = null;
// $FlowFixMe[unused-promise]
this._updateBottomIfNecessary();
};
_onLayout = async (event: ViewLayoutEvent) => {
event.persist();
const oldFrame = this._frame;
this._frame = event.nativeEvent.layout;
if (!this._initialFrameHeight) {
// save the initial frame height, before the keyboard is visible
this._initialFrameHeight = this._frame.height;
}
// update bottom height for the first time or when the height is changed
if (!oldFrame || oldFrame.height !== this._frame.height) {
await this._updateBottomIfNecessary();
}
if (this.props.onLayout) {
this.props.onLayout(event);
}
};
// Avoid unnecessary renders if the KeyboardAvoidingView is disabled.
_setBottom = (value: number) => {
const enabled = this.props.enabled ?? true;
this._bottom = value;
if (enabled) {
this.setState({bottom: value});
}
};
_updateBottomIfNecessary = async () => {
if (this._keyboardEvent == null) {
this._setBottom(0);
return;
}
const {duration, easing, endCoordinates} = this._keyboardEvent;
const height = await this._relativeKeyboardHeight(endCoordinates);
if (this._bottom === height) {
return;
}
this._setBottom(height);
const enabled = this.props.enabled ?? true;
if (enabled && duration && easing) {
LayoutAnimation.configureNext({
// We have to pass the duration equal to minimal accepted duration defined here: RCTLayoutAnimation.m
duration: duration > 10 ? duration : 10,
update: {
duration: duration > 10 ? duration : 10,
type: LayoutAnimation.Types[easing] || 'keyboard',
},
});
}
};
componentDidUpdate(
_: KeyboardAvoidingViewProps,
prevState: KeyboardAvoidingViewState,
): void {
const enabled = this.props.enabled ?? true;
if (enabled && this._bottom !== prevState.bottom) {
this.setState({bottom: this._bottom});
}
}
componentDidMount(): void {
if (!Keyboard.isVisible()) {
this._keyboardEvent = null;
this._setBottom(0);
}
if (Platform.OS === 'ios') {
this._subscriptions = [
// When undocked, split or floating, iOS will emit
// UIKeyboardWillHideNotification notification.
// UIKeyboardWillChangeFrameNotification will be emitted before
// UIKeyboardWillHideNotification, so we need to listen to
// keyboardWillHide and keyboardWillShow instead of
// keyboardWillChangeFrame.
Keyboard.addListener('keyboardWillHide', this._onKeyboardHide),
Keyboard.addListener('keyboardWillShow', this._onKeyboardChange),
];
} else {
this._subscriptions = [
Keyboard.addListener('keyboardDidHide', this._onKeyboardChange),
Keyboard.addListener('keyboardDidShow', this._onKeyboardChange),
];
}
}
componentWillUnmount(): void {
this._subscriptions.forEach(subscription => {
subscription.remove();
});
}
render(): React.Node {
const {
behavior,
children,
contentContainerStyle,
enabled = true,
// eslint-disable-next-line no-unused-vars
keyboardVerticalOffset = 0,
style,
onLayout,
...props
} = this.props;
const bottomHeight = enabled === true ? this.state.bottom : 0;
switch (behavior) {
case 'height':
let heightStyle;
if (this._frame != null && this.state.bottom > 0) {
// Note that we only apply a height change when there is keyboard present,
// i.e. this.state.bottom is greater than 0. If we remove that condition,
// this.frame.height will never go back to its original value.
// When height changes, we need to disable flex.
heightStyle = {
height: this._initialFrameHeight - bottomHeight,
flex: 0,
};
}
return (
<View
ref={this.viewRef}
style={StyleSheet.compose(style, heightStyle)}
onLayout={this._onLayout}
{...props}>
{children}
</View>
);
case 'position':
return (
<View
ref={this.viewRef}
style={style}
onLayout={this._onLayout}
{...props}>
<View
style={StyleSheet.compose(contentContainerStyle, {
bottom: bottomHeight,
})}>
{children}
</View>
</View>
);
case 'padding':
return (
<View
ref={this.viewRef}
style={StyleSheet.compose(style, {paddingBottom: bottomHeight})}
onLayout={this._onLayout}
{...props}>
{children}
</View>
);
default:
return (
<View
ref={this.viewRef}
onLayout={this._onLayout}
style={style}
{...props}>
{children}
</View>
);
}
}
}
export default KeyboardAvoidingView;

View File

@@ -0,0 +1,12 @@
/**
* 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.
*
* @flow strict
* @format
*/
export * from '../../../src/private/specs_DEPRECATED/modules/NativeKeyboardObserver';
export {default} from '../../../src/private/specs_DEPRECATED/modules/NativeKeyboardObserver';

View File

@@ -0,0 +1,21 @@
/**
* 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
*/
import type * as React from 'react';
type LayoutConformanceProps = {
/**
* strict: Layout in accordance with W3C spec, even when breaking
* compatibility: Layout with the same behavior as previous versions of React Native
*/
mode: 'strict' | 'compatibility';
children: React.ReactNode;
};
export const experimental_LayoutConformance: React.ComponentType<LayoutConformanceProps>;

View File

@@ -0,0 +1,60 @@
/**
* 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.
*
* @flow strict-local
* @format
*/
import StyleSheet from '../../StyleSheet/StyleSheet';
import LayoutConformanceNativeComponent from './LayoutConformanceNativeComponent';
import * as React from 'react';
export type LayoutConformanceProps = $ReadOnly<{
/**
* strict: Layout in accordance with W3C spec, even when breaking
* compatibility: Layout with the same behavior as previous versions of React Native
*/
mode: 'strict' | 'compatibility',
children: React.Node,
}>;
// We want a graceful fallback for apps using legacy arch, but need to know
// ahead of time whether the component is available, so we test for global.
// This does not correctly handle mixed arch apps (which is okay, since we just
// degrade the error experience).
const isFabricUIManagerInstalled = global?.nativeFabricUIManager != null;
function LayoutConformance(props: LayoutConformanceProps): React.Node {
return (
<LayoutConformanceNativeComponent {...props} style={styles.container} />
);
}
function UnimplementedLayoutConformance(
props: LayoutConformanceProps,
): React.Node {
if (__DEV__) {
const warnOnce = require('../../Utilities/warnOnce').default;
warnOnce(
'layoutconformance-unsupported',
'"LayoutConformance" is only supported in the New Architecture',
);
}
return props.children;
}
export default (isFabricUIManagerInstalled
? LayoutConformance
: UnimplementedLayoutConformance) as component(...LayoutConformanceProps);
const styles = StyleSheet.create({
container: {
display: 'contents',
},
});

View File

@@ -0,0 +1,29 @@
/**
* 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.
*
* @flow strict-local
* @format
*/
import type {HostComponent} from '../../../src/private/types/HostComponent';
import type {ViewProps} from '../View/ViewPropTypes';
import * as NativeComponentRegistry from '../../NativeComponent/NativeComponentRegistry';
type Props = $ReadOnly<{
mode: 'strict' | 'compatibility',
...ViewProps,
}>;
const LayoutConformanceNativeComponent: HostComponent<Props> =
NativeComponentRegistry.get<Props>('LayoutConformance', () => ({
uiViewClassName: 'LayoutConformance',
validAttributes: {
mode: true,
},
}));
export default LayoutConformanceNativeComponent;

View File

@@ -0,0 +1,167 @@
/**
* 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
*/
import type * as React from 'react';
import {Insets} from '../../../types/public/Insets';
import {ColorValue, StyleProp} from '../../StyleSheet/StyleSheet';
import {ViewStyle} from '../../StyleSheet/StyleSheetTypes';
import {
GestureResponderEvent,
MouseEvent,
NativeSyntheticEvent,
TargetedEvent,
} from '../../Types/CoreEventTypes';
import {View} from '../View/View';
import {AccessibilityProps} from '../View/ViewAccessibility';
import {ViewProps} from '../View/ViewPropTypes';
export interface PressableStateCallbackType {
readonly pressed: boolean;
}
export interface PressableAndroidRippleConfig {
color?: null | ColorValue | undefined;
borderless?: null | boolean | undefined;
radius?: null | number | undefined;
foreground?: null | boolean | undefined;
}
export interface PressableProps
extends AccessibilityProps,
Omit<ViewProps, 'children' | 'style' | 'hitSlop'> {
/**
* Called when the hover is activated to provide visual feedback.
*/
onHoverIn?: null | ((event: MouseEvent) => void) | undefined;
/**
* Called when the hover is deactivated to undo visual feedback.
*/
onHoverOut?: null | ((event: MouseEvent) => void) | undefined;
/**
* Called when a single tap gesture is detected.
*/
onPress?: null | ((event: GestureResponderEvent) => void) | undefined;
/**
* Called when a touch is engaged before `onPress`.
*/
onPressIn?: null | ((event: GestureResponderEvent) => void) | undefined;
/**
* Called when a touch is released before `onPress`.
*/
onPressOut?: null | ((event: GestureResponderEvent) => void) | undefined;
/**
* Called when a long-tap gesture is detected.
*/
onLongPress?: null | ((event: GestureResponderEvent) => void) | undefined;
/**
* Called after the element loses focus.
* @platform macos windows
*/
onBlur?:
| null
| ((event: NativeSyntheticEvent<TargetedEvent>) => void)
| undefined;
/**
* Called after the element is focused.
* @platform macos windows
*/
onFocus?:
| null
| ((event: NativeSyntheticEvent<TargetedEvent>) => void)
| undefined;
/**
* Either children or a render prop that receives a boolean reflecting whether
* the component is currently pressed.
*/
children?:
| React.ReactNode
| ((state: PressableStateCallbackType) => React.ReactNode)
| undefined;
/**
* Whether a press gesture can be interrupted by a parent gesture such as a
* scroll event. Defaults to true.
*/
cancelable?: null | boolean | undefined;
/**
* Duration to wait after hover in before calling `onHoverIn`.
* @platform macos windows
*/
delayHoverIn?: number | null | undefined;
/**
* Duration to wait after hover out before calling `onHoverOut`.
* @platform macos windows
*/
delayHoverOut?: number | null | undefined;
/**
* Duration (in milliseconds) from `onPressIn` before `onLongPress` is called.
*/
delayLongPress?: null | number | undefined;
/**
* Whether the press behavior is disabled.
*/
disabled?: null | boolean | undefined;
/**
* Additional distance outside of this view in which a press is detected.
*/
hitSlop?: null | Insets | number | undefined;
/**
* Additional distance outside of this view in which a touch is considered a
* press before `onPressOut` is triggered.
*/
pressRetentionOffset?: null | Insets | number | undefined;
/**
* If true, doesn't play system sound on touch.
*/
android_disableSound?: null | boolean | undefined;
/**
* Enables the Android ripple effect and configures its color.
*/
android_ripple?: null | PressableAndroidRippleConfig | undefined;
/**
* Used only for documentation or testing (e.g. snapshot testing).
*/
testOnly_pressed?: null | boolean | undefined;
/**
* Either view styles or a function that receives a boolean reflecting whether
* the component is currently pressed and returns view styles.
*/
style?:
| StyleProp<ViewStyle>
| ((state: PressableStateCallbackType) => StyleProp<ViewStyle>)
| undefined;
/**
* Duration (in milliseconds) to wait after press down before calling onPressIn.
*/
unstable_pressDelay?: number | undefined;
}
// TODO use React.AbstractComponent when available
export const Pressable: React.ForwardRefExoticComponent<
PressableProps & React.RefAttributes<View>
>;

View File

@@ -0,0 +1,358 @@
/**
* 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.
*
* @flow strict-local
* @format
*/
import type {ViewStyleProp} from '../../StyleSheet/StyleSheet';
import type {
GestureResponderEvent,
LayoutChangeEvent,
MouseEvent,
} from '../../Types/CoreEventTypes';
import type {ViewProps} from '../View/ViewPropTypes';
import {PressabilityDebugView} from '../../Pressability/PressabilityDebug';
import usePressability from '../../Pressability/usePressability';
import {type RectOrSize} from '../../StyleSheet/Rect';
import useMergeRefs from '../../Utilities/useMergeRefs';
import View from '../View/View';
import useAndroidRippleForView, {
type PressableAndroidRippleConfig,
} from './useAndroidRippleForView';
import * as React from 'react';
import {memo, useMemo, useRef, useState} from 'react';
export type {PressableAndroidRippleConfig};
export type PressableStateCallbackType = $ReadOnly<{
pressed: boolean,
}>;
type PressableBaseProps = $ReadOnly<{
/**
* Whether a press gesture can be interrupted by a parent gesture such as a
* scroll event. Defaults to true.
*/
cancelable?: ?boolean,
/**
* Either children or a render prop that receives a boolean reflecting whether
* the component is currently pressed.
*/
children?: React.Node | ((state: PressableStateCallbackType) => React.Node),
/**
* Duration to wait after hover in before calling `onHoverIn`.
*/
delayHoverIn?: ?number,
/**
* Duration to wait after hover out before calling `onHoverOut`.
*/
delayHoverOut?: ?number,
/**
* Duration (in milliseconds) from `onPressIn` before `onLongPress` is called.
*/
delayLongPress?: ?number,
/**
* Whether the press behavior is disabled.
*/
disabled?: ?boolean,
/**
* Additional distance outside of this view in which a press is detected.
*/
hitSlop?: ?RectOrSize,
/**
* Additional distance outside of this view in which a touch is considered a
* press before `onPressOut` is triggered.
*/
pressRetentionOffset?: ?RectOrSize,
/**
* Called when this view's layout changes.
*/
onLayout?: ?(event: LayoutChangeEvent) => mixed,
/**
* Called when the hover is activated to provide visual feedback.
*/
onHoverIn?: ?(event: MouseEvent) => mixed,
/**
* Called when the hover is deactivated to undo visual feedback.
*/
onHoverOut?: ?(event: MouseEvent) => mixed,
/**
* Called when a long-tap gesture is detected.
*/
onLongPress?: ?(event: GestureResponderEvent) => mixed,
/**
* Called when a single tap gesture is detected.
*/
onPress?: ?(event: GestureResponderEvent) => mixed,
/**
* Called when a touch is engaged before `onPress`.
*/
onPressIn?: ?(event: GestureResponderEvent) => mixed,
/**
* Called when the press location moves.
*/
onPressMove?: ?(event: GestureResponderEvent) => mixed,
/**
* Called when a touch is released before `onPress`.
*/
onPressOut?: ?(event: GestureResponderEvent) => mixed,
/**
* Whether to prevent any other native components from becoming responder
* while this pressable is responder.
*/
blockNativeResponder?: ?boolean,
/**
* Either view styles or a function that receives a boolean reflecting whether
* the component is currently pressed and returns view styles.
*/
style?:
| ViewStyleProp
| ((state: PressableStateCallbackType) => ViewStyleProp),
/**
* Identifier used to find this view in tests.
*/
testID?: ?string,
/**
* If true, doesn't play system sound on touch.
*/
android_disableSound?: ?boolean,
/**
* Enables the Android ripple effect and configures its color.
*/
android_ripple?: ?PressableAndroidRippleConfig,
/**
* Used only for documentation or testing (e.g. snapshot testing).
*/
testOnly_pressed?: ?boolean,
/**
* Duration to wait after press down before calling `onPressIn`.
*/
unstable_pressDelay?: ?number,
}>;
export type PressableProps = $ReadOnly<{
// Pressability may override `onMouseEnter` and `onMouseLeave` to
// implement `onHoverIn` and `onHoverOut` in a platform-agnostic way.
// Hover events should be used instead of mouse events.
...Omit<ViewProps, 'onMouseEnter' | 'onMouseLeave'>,
...PressableBaseProps,
}>;
type Instance = React.ElementRef<typeof View>;
/**
* Component used to build display components that should respond to whether the
* component is currently pressed or not.
*/
function Pressable({
ref: forwardedRef,
...props
}: {
ref?: React.RefSetter<Instance>,
...PressableProps,
}): React.Node {
const {
accessible,
accessibilityState,
'aria-live': ariaLive,
android_disableSound,
android_ripple,
'aria-busy': ariaBusy,
'aria-checked': ariaChecked,
'aria-disabled': ariaDisabled,
'aria-expanded': ariaExpanded,
'aria-label': ariaLabel,
'aria-selected': ariaSelected,
blockNativeResponder,
cancelable,
children,
delayHoverIn,
delayHoverOut,
delayLongPress,
disabled,
focusable,
hitSlop,
onBlur,
onFocus,
onHoverIn,
onHoverOut,
onLongPress,
onPress,
onPressIn,
onPressMove,
onPressOut,
pressRetentionOffset,
style,
testOnly_pressed,
unstable_pressDelay,
...restProps
} = props;
const viewRef = useRef<Instance | null>(null);
const mergedRef = useMergeRefs(forwardedRef, viewRef);
const android_rippleConfig = useAndroidRippleForView(android_ripple, viewRef);
const [pressed, setPressed] = usePressState(testOnly_pressed === true);
const shouldUpdatePressed =
typeof children === 'function' || typeof style === 'function';
let _accessibilityState = {
busy: ariaBusy ?? accessibilityState?.busy,
checked: ariaChecked ?? accessibilityState?.checked,
disabled: ariaDisabled ?? accessibilityState?.disabled,
expanded: ariaExpanded ?? accessibilityState?.expanded,
selected: ariaSelected ?? accessibilityState?.selected,
};
_accessibilityState =
disabled != null ? {..._accessibilityState, disabled} : _accessibilityState;
const accessibilityValue = {
max: props['aria-valuemax'] ?? props.accessibilityValue?.max,
min: props['aria-valuemin'] ?? props.accessibilityValue?.min,
now: props['aria-valuenow'] ?? props.accessibilityValue?.now,
text: props['aria-valuetext'] ?? props.accessibilityValue?.text,
};
const accessibilityLiveRegion =
ariaLive === 'off' ? 'none' : (ariaLive ?? props.accessibilityLiveRegion);
const accessibilityLabel = ariaLabel ?? props.accessibilityLabel;
const restPropsWithDefaults: React.ElementConfig<typeof View> = {
...restProps,
...android_rippleConfig?.viewProps,
accessible: accessible !== false,
accessibilityViewIsModal:
restProps['aria-modal'] ?? restProps.accessibilityViewIsModal,
accessibilityLiveRegion,
accessibilityLabel,
accessibilityState: _accessibilityState,
focusable: focusable !== false,
accessibilityValue,
hitSlop,
};
const config = useMemo(
() => ({
cancelable,
disabled,
hitSlop,
pressRectOffset: pressRetentionOffset,
android_disableSound,
delayHoverIn,
delayHoverOut,
delayLongPress,
delayPressIn: unstable_pressDelay,
onBlur,
onFocus,
onHoverIn,
onHoverOut,
onLongPress,
onPress,
onPressIn(event: GestureResponderEvent): void {
if (android_rippleConfig != null) {
android_rippleConfig.onPressIn(event);
}
shouldUpdatePressed && setPressed(true);
if (onPressIn != null) {
onPressIn(event);
}
},
onPressMove(event: GestureResponderEvent): void {
android_rippleConfig?.onPressMove(event);
if (onPressMove != null) {
onPressMove(event);
}
},
onPressOut(event: GestureResponderEvent): void {
if (android_rippleConfig != null) {
android_rippleConfig.onPressOut(event);
}
shouldUpdatePressed && setPressed(false);
if (onPressOut != null) {
onPressOut(event);
}
},
blockNativeResponder,
}),
[
android_disableSound,
android_rippleConfig,
blockNativeResponder,
cancelable,
delayHoverIn,
delayHoverOut,
delayLongPress,
disabled,
hitSlop,
onBlur,
onFocus,
onHoverIn,
onHoverOut,
onLongPress,
onPress,
onPressIn,
onPressMove,
onPressOut,
pressRetentionOffset,
setPressed,
shouldUpdatePressed,
unstable_pressDelay,
],
);
const eventHandlers = usePressability(config);
return (
<View
{...restPropsWithDefaults}
{...eventHandlers}
ref={mergedRef}
style={typeof style === 'function' ? style({pressed}) : style}
collapsable={false}>
{typeof children === 'function' ? children({pressed}) : children}
{__DEV__ ? <PressabilityDebugView color="red" hitSlop={hitSlop} /> : null}
</View>
);
}
function usePressState(forcePressed: boolean): [boolean, (boolean) => void] {
const [pressed, setPressed] = useState(false);
return [pressed || forcePressed, setPressed];
}
const MemoedPressable = memo(Pressable);
MemoedPressable.displayName = 'Pressable';
export default (MemoedPressable: component(
ref?: React.RefSetter<React.ElementRef<typeof View>>,
...props: PressableProps
));

View File

@@ -0,0 +1,109 @@
/**
* 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.
*
* @flow strict-local
* @format
*/
import type {ColorValue} from '../../StyleSheet/StyleSheet';
import type {GestureResponderEvent} from '../../Types/CoreEventTypes';
import processColor from '../../StyleSheet/processColor';
import Platform from '../../Utilities/Platform';
import View from '../View/View';
import {Commands} from '../View/ViewNativeComponent';
import invariant from 'invariant';
import * as React from 'react';
import {useMemo} from 'react';
type NativeBackgroundProp = $ReadOnly<{
type: 'RippleAndroid',
color: ?number,
borderless: boolean,
rippleRadius: ?number,
}>;
export type PressableAndroidRippleConfig = {
color?: ColorValue,
borderless?: boolean,
radius?: number,
foreground?: boolean,
};
/**
* Provides the event handlers and props for configuring the ripple effect on
* supported versions of Android.
*/
export default function useAndroidRippleForView(
rippleConfig: ?PressableAndroidRippleConfig,
viewRef: {current: null | React.ElementRef<typeof View>},
): ?$ReadOnly<{
onPressIn: (event: GestureResponderEvent) => void,
onPressMove: (event: GestureResponderEvent) => void,
onPressOut: (event: GestureResponderEvent) => void,
viewProps:
| $ReadOnly<{nativeBackgroundAndroid: NativeBackgroundProp}>
| $ReadOnly<{nativeForegroundAndroid: NativeBackgroundProp}>,
}> {
const {color, borderless, radius, foreground} = rippleConfig ?? {};
return useMemo(() => {
if (
Platform.OS === 'android' &&
(color != null || borderless != null || radius != null)
) {
const processedColor = processColor(color);
invariant(
processedColor == null || typeof processedColor === 'number',
'Unexpected color given for Ripple color',
);
const nativeRippleValue = {
type: 'RippleAndroid',
color: processedColor,
borderless: borderless === true,
rippleRadius: radius,
};
return {
viewProps:
foreground === true
? // $FlowFixMe[incompatible-type]
{nativeForegroundAndroid: nativeRippleValue}
: // $FlowFixMe[incompatible-type]
{nativeBackgroundAndroid: nativeRippleValue},
onPressIn(event: GestureResponderEvent): void {
const view = viewRef.current;
if (view != null) {
Commands.hotspotUpdate(
view,
event.nativeEvent.locationX ?? 0,
event.nativeEvent.locationY ?? 0,
);
Commands.setPressed(view, true);
}
},
onPressMove(event: GestureResponderEvent): void {
const view = viewRef.current;
if (view != null) {
Commands.hotspotUpdate(
view,
event.nativeEvent.locationX ?? 0,
event.nativeEvent.locationY ?? 0,
);
}
},
onPressOut(event: GestureResponderEvent): void {
const view = viewRef.current;
if (view != null) {
Commands.setPressed(view, false);
}
},
};
}
return null;
}, [borderless, color, foreground, radius, viewRef]);
}

View File

@@ -0,0 +1,71 @@
/**
* 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.
*
* @flow strict-local
* @format
*/
import type {ProgressBarAndroidProps} from './ProgressBarAndroidTypes';
import ProgressBarAndroidNativeComponent from './ProgressBarAndroidNativeComponent';
const React = require('react');
export type {ProgressBarAndroidProps};
/**
* React component that wraps the Android-only `ProgressBar`. This component is
* used to indicate that the app is loading or there is activity in the app.
*
* Example:
*
* ```
* render: function() {
* var progressBar =
* <View style={styles.container}>
* <ProgressBar styleAttr="Inverse" />
* </View>;
* return (
* <MyLoadingComponent
* componentView={componentView}
* loadingView={progressBar}
* style={styles.loadingComponent}
* />
* );
* },
* ```
*/
const ProgressBarAndroid: component(
ref?: React.RefSetter<
React.ElementRef<typeof ProgressBarAndroidNativeComponent>,
>,
...props: ProgressBarAndroidProps
) = function ProgressBarAndroid({
ref: forwardedRef,
// $FlowFixMe[incompatible-type]
styleAttr = 'Normal',
indeterminate = true,
animating = true,
...restProps
}: {
ref?: React.RefSetter<
React.ElementRef<typeof ProgressBarAndroidNativeComponent>,
>,
...ProgressBarAndroidProps,
}) {
return (
<ProgressBarAndroidNativeComponent
styleAttr={styleAttr}
indeterminate={indeterminate}
animating={animating}
{...restProps}
ref={forwardedRef}
/>
);
};
export default ProgressBarAndroid;

View File

@@ -0,0 +1,83 @@
/**
* 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
*/
import type * as React from 'react';
import {Constructor} from '../../../types/private/Utilities';
import {HostInstance} from '../../../types/public/ReactNativeTypes';
import {ColorValue} from '../../StyleSheet/StyleSheet';
import {ViewProps} from '../View/ViewPropTypes';
/**
* ProgressBarAndroid has been extracted from react-native core and will be removed in a future release.
* It can now be installed and imported from `@react-native-community/progress-bar-android` instead of 'react-native'.
* @see https://github.com/react-native-community/progress-bar-android
* @deprecated
*/
export interface ProgressBarAndroidProps extends ViewProps {
/**
* Style of the ProgressBar. One of:
Horizontal
Normal (default)
Small
Large
Inverse
SmallInverse
LargeInverse
*/
styleAttr?:
| 'Horizontal'
| 'Normal'
| 'Small'
| 'Large'
| 'Inverse'
| 'SmallInverse'
| 'LargeInverse'
| undefined;
/**
* If the progress bar will show indeterminate progress.
* Note that this can only be false if styleAttr is Horizontal.
*/
indeterminate?: boolean | undefined;
/**
* The progress value (between 0 and 1).
*/
progress?: number | undefined;
/**
* Whether to show the ProgressBar (true, the default) or hide it (false).
*/
animating?: boolean | undefined;
/**
* Color of the progress bar.
*/
color?: ColorValue | undefined;
/**
* Used to locate this view in end-to-end tests.
*/
testID?: string | undefined;
}
/**
* React component that wraps the Android-only `ProgressBar`. This component is used to indicate
* that the app is loading or there is some activity in the app.
*/
declare class ProgressBarAndroidComponent extends React.Component<ProgressBarAndroidProps> {}
declare const ProgressBarAndroidBase: Constructor<HostInstance> &
typeof ProgressBarAndroidComponent;
/**
* ProgressBarAndroid has been extracted from react-native core and will be removed in a future release.
* It can now be installed and imported from `@react-native-community/progress-bar-android` instead of 'react-native'.
* @see https://github.com/react-native-progress-view/progress-bar-android
* @deprecated
*/
export class ProgressBarAndroid extends ProgressBarAndroidBase {}

View File

@@ -0,0 +1,46 @@
/**
* 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.
*
* @flow strict-local
* @format
*/
'use strict';
import typeof ProgressBarAndroidNativeComponentType from './ProgressBarAndroidNativeComponent';
import type {ProgressBarAndroidProps} from './ProgressBarAndroidTypes';
import Platform from '../../Utilities/Platform';
export type {ProgressBarAndroidProps};
// A utility type to preserve the semantics of the union uses in the definition
// of ProgressBarAndroidProps. TS's Omit does not distribute over unions, so
// we define our own version which does. This does not affect Flow.
// $FlowExpectedError[unclear-type]
type Omit<T, K> = T extends any ? Pick<T, Exclude<$Keys<T>, K>> : T;
/**
* ProgressBarAndroid has been extracted from react-native core and will be removed in a future release.
* It can now be installed and imported from `@react-native-community/progress-bar-android` instead of 'react-native'.
* @see https://github.com/react-native-community/progress-bar-android
* @deprecated
*/
let ProgressBarAndroid: component(
ref?: React.RefSetter<
React.ElementRef<ProgressBarAndroidNativeComponentType>,
>,
...props: Omit<ProgressBarAndroidProps, empty>
);
if (Platform.OS === 'android') {
ProgressBarAndroid = require('./ProgressBarAndroid').default;
} else {
ProgressBarAndroid = require('../UnimplementedViews/UnimplementedView')
.default as $FlowFixMe;
}
export default ProgressBarAndroid;

View File

@@ -0,0 +1,12 @@
/**
* 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.
*
* @flow strict-local
* @format
*/
export * from '../../../src/private/specs_DEPRECATED/components/ProgressBarAndroidNativeComponent';
export {default} from '../../../src/private/specs_DEPRECATED/components/ProgressBarAndroidNativeComponent';

View File

@@ -0,0 +1,63 @@
/**
* 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.
*
* @flow strict-local
* @format
*/
import type {ColorValue} from '../../StyleSheet/StyleSheet';
import type {ViewProps} from '../View/ViewPropTypes';
/**
* Style of the ProgressBar and whether it shows indeterminate progress (e.g. spinner).
*
* `indeterminate` can only be false if `styleAttr` is Horizontal, and requires a
* `progress` value.
*/
type DeterminateProgressBarAndroidStyleAttrProp = {
styleAttr: 'Horizontal',
indeterminate: false,
progress: number,
};
type IndeterminateProgressBarAndroidStyleAttrProp = {
styleAttr:
| 'Horizontal'
| 'Normal'
| 'Small'
| 'Large'
| 'Inverse'
| 'SmallInverse'
| 'LargeInverse',
indeterminate: true,
};
type ProgressBarAndroidBaseProps = $ReadOnly<{
/**
* Whether to show the ProgressBar (true, the default) or hide it (false).
*/
animating?: ?boolean,
/**
* Color of the progress bar.
*/
color?: ?ColorValue,
/**
* Used to locate this view in end-to-end tests.
*/
testID?: ?string,
}>;
export type ProgressBarAndroidProps =
| $ReadOnly<{
...ViewProps,
...ProgressBarAndroidBaseProps,
...DeterminateProgressBarAndroidStyleAttrProp,
}>
| $ReadOnly<{
...ViewProps,
...ProgressBarAndroidBaseProps,
...IndeterminateProgressBarAndroidStyleAttrProp,
}>;

View File

@@ -0,0 +1,12 @@
/**
* 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.
*
* @flow strict-local
* @format
*/
export * from '../../../src/private/specs_DEPRECATED/components/AndroidSwipeRefreshLayoutNativeComponent';
export {default} from '../../../src/private/specs_DEPRECATED/components/AndroidSwipeRefreshLayoutNativeComponent';

View File

@@ -0,0 +1,14 @@
/**
* 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.
*
* @flow strict-local
* @format
*/
export * from '../../../src/private/specs_DEPRECATED/components/PullToRefreshViewNativeComponent';
import PullToRefreshViewNativeComponent from '../../../src/private/specs_DEPRECATED/components/PullToRefreshViewNativeComponent';
export default PullToRefreshViewNativeComponent;

View File

@@ -0,0 +1,87 @@
/**
* 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
*/
import type * as React from 'react';
import {Constructor} from '../../../types/private/Utilities';
import {HostInstance} from '../../../types/public/ReactNativeTypes';
import {ColorValue} from '../../StyleSheet/StyleSheet';
import {ViewProps} from '../View/ViewPropTypes';
export interface RefreshControlPropsIOS extends ViewProps {
/**
* The color of the refresh indicator.
*/
tintColor?: ColorValue | undefined;
/**
* The title displayed under the refresh indicator.
*/
title?: string | undefined;
/**
* Title color.
*/
titleColor?: ColorValue | undefined;
}
export interface RefreshControlPropsAndroid extends ViewProps {
/**
* The colors (at least one) that will be used to draw the refresh indicator.
*/
colors?: ColorValue[] | undefined;
/**
* Whether the pull to refresh functionality is enabled.
*/
enabled?: boolean | undefined;
/**
* The background color of the refresh indicator.
*/
progressBackgroundColor?: ColorValue | undefined;
/**
* Size of the refresh indicator, see RefreshControl.SIZE.
*/
size?: 'default' | 'large' | undefined;
}
export interface RefreshControlProps
extends RefreshControlPropsIOS,
RefreshControlPropsAndroid {
/**
* Called when the view starts refreshing.
*/
onRefresh?: (() => void) | undefined;
/**
* Whether the view should be indicating an active refresh.
*/
refreshing: boolean;
/**
* Progress view top offset
*/
progressViewOffset?: number | undefined;
}
/**
* This component is used inside a ScrollView or ListView to add pull to refresh
* functionality. When the ScrollView is at `scrollY: 0`, swiping down
* triggers an `onRefresh` event.
*
* __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.
*/
declare class RefreshControlComponent extends React.Component<RefreshControlProps> {}
declare const RefreshControlBase: Constructor<HostInstance> &
typeof RefreshControlComponent;
export class RefreshControl extends RefreshControlBase {
static SIZE: Object; // Undocumented
}

View File

@@ -0,0 +1,207 @@
/**
* 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.
*
* @flow strict-local
* @format
*/
import type {ColorValue} from '../../StyleSheet/StyleSheet';
import type {ViewProps} from '../View/ViewPropTypes';
import AndroidSwipeRefreshLayoutNativeComponent, {
Commands as AndroidSwipeRefreshLayoutCommands,
} from './AndroidSwipeRefreshLayoutNativeComponent';
import PullToRefreshViewNativeComponent, {
Commands as PullToRefreshCommands,
} from './PullToRefreshViewNativeComponent';
import * as React from 'react';
const Platform = require('../../Utilities/Platform').default;
export type RefreshControlPropsIOS = $ReadOnly<{
/**
* The color of the refresh indicator.
*/
tintColor?: ?ColorValue,
/**
* Title color.
*/
titleColor?: ?ColorValue,
/**
* The title displayed under the refresh indicator.
*/
title?: ?string,
}>;
export type RefreshControlPropsAndroid = $ReadOnly<{
/**
* Whether the pull to refresh functionality is enabled.
*/
enabled?: ?boolean,
/**
* The colors (at least one) that will be used to draw the refresh indicator.
*/
colors?: ?$ReadOnlyArray<ColorValue>,
/**
* The background color of the refresh indicator.
*/
progressBackgroundColor?: ?ColorValue,
/**
* Size of the refresh indicator.
*/
size?: ?('default' | 'large'),
}>;
type RefreshControlBaseProps = $ReadOnly<{
/**
* Called when the view starts refreshing.
*/
onRefresh?: ?() => void | Promise<void>,
/**
* Whether the view should be indicating an active refresh.
*/
refreshing: boolean,
/**
* Progress view top offset
*/
progressViewOffset?: ?number,
}>;
export type RefreshControlProps = $ReadOnly<{
...ViewProps,
...RefreshControlPropsIOS,
...RefreshControlPropsAndroid,
...RefreshControlBaseProps,
}>;
/**
* This component is used inside a ScrollView or ListView to add pull to refresh
* functionality. When the ScrollView is at `scrollY: 0`, swiping down
* triggers an `onRefresh` event.
*
* ### Usage example
*
* ``` js
* class RefreshableList extends Component {
* constructor(props) {
* super(props);
* this.state = {
* refreshing: false,
* };
* }
*
* _onRefresh() {
* this.setState({refreshing: true});
* fetchData().then(() => {
* this.setState({refreshing: false});
* });
* }
*
* render() {
* return (
* <ListView
* refreshControl={
* <RefreshControl
* refreshing={this.state.refreshing}
* onRefresh={this._onRefresh.bind(this)}
* />
* }
* ...
* >
* ...
* </ListView>
* );
* }
* ...
* }
* ```
*
* __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.
*/
class RefreshControl extends React.Component<RefreshControlProps> {
_nativeRef: ?React.ElementRef<
| typeof PullToRefreshViewNativeComponent
| typeof AndroidSwipeRefreshLayoutNativeComponent,
>;
_lastNativeRefreshing: boolean = false;
componentDidMount() {
this._lastNativeRefreshing = this.props.refreshing;
}
componentDidUpdate(prevProps: RefreshControlProps) {
// RefreshControl is a controlled component so if the native refreshing
// value doesn't match the current js refreshing prop update it to
// the js value.
if (this.props.refreshing !== prevProps.refreshing) {
this._lastNativeRefreshing = this.props.refreshing;
} else if (
this.props.refreshing !== this._lastNativeRefreshing &&
this._nativeRef
) {
if (Platform.OS === 'android') {
AndroidSwipeRefreshLayoutCommands.setNativeRefreshing(
this._nativeRef,
this.props.refreshing,
);
} else {
PullToRefreshCommands.setNativeRefreshing(
this._nativeRef,
this.props.refreshing,
);
}
this._lastNativeRefreshing = this.props.refreshing;
}
}
render(): React.Node {
if (Platform.OS === 'ios') {
const {enabled, colors, progressBackgroundColor, size, ...props} =
this.props;
return (
<PullToRefreshViewNativeComponent
{...props}
ref={this._setNativeRef}
onRefresh={this._onRefresh}
/>
);
} else {
const {tintColor, titleColor, title, ...props} = this.props;
return (
<AndroidSwipeRefreshLayoutNativeComponent
{...props}
ref={this._setNativeRef}
onRefresh={this._onRefresh}
/>
);
}
}
_onRefresh = () => {
this._lastNativeRefreshing = true;
// $FlowFixMe[unused-promise]
this.props.onRefresh && this.props.onRefresh();
// The native component will start refreshing so force an update to
// make sure it stays in sync with the js component.
this.forceUpdate();
};
_setNativeRef = (
ref: ?React.ElementRef<
| typeof PullToRefreshViewNativeComponent
| typeof AndroidSwipeRefreshLayoutNativeComponent,
>,
) => {
this._nativeRef = ref;
};
}
export default RefreshControl;

View File

@@ -0,0 +1,12 @@
/**
* 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.
*
* @flow strict-local
* @format
*/
export * from '../../../src/private/specs_DEPRECATED/components/RCTSafeAreaViewNativeComponent';
export {default} from '../../../src/private/specs_DEPRECATED/components/RCTSafeAreaViewNativeComponent';

View File

@@ -0,0 +1,33 @@
/**
* 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
*/
import type * as React from 'react';
import {Constructor} from '../../../types/private/Utilities';
import {HostInstance} from '../../../types/public/ReactNativeTypes';
import {ViewProps} from '../View/ViewPropTypes';
/**
* @deprecated
* Use `react-native-safe-area-context` instead. This component is deprecated and will be removed in a future release.
*
* Renders nested content and automatically applies paddings reflect the portion of the view
* that is not covered by navigation bars, tab bars, toolbars, and other ancestor views.
* Moreover, and most importantly, Safe Area's paddings reflect physical limitation of the screen,
* such as rounded corners or camera notches (aka sensor housing area on iPhone X).
*/
declare class SafeAreaViewComponent extends React.Component<ViewProps> {}
declare const SafeAreaViewBase: Constructor<HostInstance> &
typeof SafeAreaViewComponent;
/**
* @deprecated
* Use `react-native-safe-area-context` instead. This component is deprecated and will be removed in a future release.
*/
export class SafeAreaView extends SafeAreaViewBase {}

View File

@@ -0,0 +1,35 @@
/**
* 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.
*
* @flow strict-local
* @format
*/
import type {ViewProps} from '../View/ViewPropTypes';
import Platform from '../../Utilities/Platform';
import View from '../View/View';
import * as React from 'react';
/**
* Renders nested content and automatically applies paddings reflect the portion
* of the view that is not covered by navigation bars, tab bars, toolbars, and
* other ancestor views.
*
* Moreover, and most importantly, Safe Area's paddings reflect physical
* limitation of the screen, such as rounded corners or camera notches (aka
* sensor housing area on iPhone X).
* @deprecated Use `react-native-safe-area-context` instead. This component will be removed in a future release.
*/
const SafeAreaView: component(
ref?: React.RefSetter<React.ElementRef<typeof View>>,
...props: ViewProps
) = Platform.select({
ios: require('./RCTSafeAreaViewNativeComponent').default,
default: View,
});
export default SafeAreaView;

View File

@@ -0,0 +1,12 @@
/**
* 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.
*
* @flow strict-local
* @format
*/
export * from '../../../src/private/specs_DEPRECATED/components/AndroidHorizontalScrollContentViewNativeComponent';
export {default} from '../../../src/private/specs_DEPRECATED/components/AndroidHorizontalScrollContentViewNativeComponent';

View File

@@ -0,0 +1,71 @@
/**
* 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.
*
* @flow strict-local
* @format
*/
import type {HostComponent} from '../../../src/private/types/HostComponent';
import type {PartialViewConfig} from '../../Renderer/shims/ReactNativeTypes';
import type {ScrollViewNativeProps as Props} from './ScrollViewNativeComponentType';
import * as NativeComponentRegistry from '../../NativeComponent/NativeComponentRegistry';
export const __INTERNAL_VIEW_CONFIG: PartialViewConfig = {
uiViewClassName: 'AndroidHorizontalScrollView',
bubblingEventTypes: {},
directEventTypes: {},
validAttributes: {
decelerationRate: true,
disableIntervalMomentum: true,
maintainVisibleContentPosition: true,
endFillColor: {process: require('../../StyleSheet/processColor').default},
fadingEdgeLength: true,
nestedScrollEnabled: true,
overScrollMode: true,
pagingEnabled: true,
persistentScrollbar: true,
horizontal: true,
scrollEnabled: true,
scrollEventThrottle: true,
scrollPerfTag: true,
sendMomentumEvents: true,
showsHorizontalScrollIndicator: true,
snapToAlignment: true,
snapToEnd: true,
snapToInterval: true,
snapToStart: true,
snapToOffsets: true,
contentOffset: true,
borderBottomLeftRadius: true,
borderBottomRightRadius: true,
borderRadius: true,
borderStyle: true,
borderRightColor: {
process: require('../../StyleSheet/processColor').default,
},
borderColor: {process: require('../../StyleSheet/processColor').default},
borderBottomColor: {
process: require('../../StyleSheet/processColor').default,
},
borderTopLeftRadius: true,
borderTopColor: {process: require('../../StyleSheet/processColor').default},
removeClippedSubviews: true,
borderTopRightRadius: true,
borderLeftColor: {
process: require('../../StyleSheet/processColor').default,
},
pointerEvents: true,
},
};
const AndroidHorizontalScrollViewNativeComponent: HostComponent<Props> =
NativeComponentRegistry.get<Props>(
'AndroidHorizontalScrollView',
() => __INTERNAL_VIEW_CONFIG,
);
export default AndroidHorizontalScrollViewNativeComponent;

View File

@@ -0,0 +1,30 @@
/**
* 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.
*
* @flow strict-local
* @format
*/
import type {HostComponent} from '../../../src/private/types/HostComponent';
import type {PartialViewConfig} from '../../Renderer/shims/ReactNativeTypes';
import type {ViewProps as Props} from '../View/ViewPropTypes';
import * as NativeComponentRegistry from '../../NativeComponent/NativeComponentRegistry';
export const __INTERNAL_VIEW_CONFIG: PartialViewConfig = {
uiViewClassName: 'RCTScrollContentView',
bubblingEventTypes: {},
directEventTypes: {},
validAttributes: {},
};
const ScrollContentViewNativeComponent: HostComponent<Props> =
NativeComponentRegistry.get<Props>(
'RCTScrollContentView',
() => __INTERNAL_VIEW_CONFIG,
);
export default ScrollContentViewNativeComponent;

View File

@@ -0,0 +1,937 @@
/**
* 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
*/
import type * as React from 'react';
import {Constructor} from '../../../types/private/Utilities';
import {Insets} from '../../../types/public/Insets';
import {HostInstance} from '../../../types/public/ReactNativeTypes';
import {ColorValue, StyleProp} from '../../StyleSheet/StyleSheet';
import {ViewStyle} from '../../StyleSheet/StyleSheetTypes';
import {
NativeSyntheticEvent,
NativeTouchEvent,
} from '../../Types/CoreEventTypes';
import {RefreshControlProps} from '../RefreshControl/RefreshControl';
import {Touchable} from '../Touchable/Touchable';
import {ViewProps} from '../View/ViewPropTypes';
import {View} from '../View/View';
// See https://reactnative.dev/docs/scrollview#contentoffset
export interface PointProp {
x: number;
y: number;
}
export interface ScrollResponderEvent
extends NativeSyntheticEvent<NativeTouchEvent> {}
interface SubscribableMixin {
/**
* Special form of calling `addListener` that *guarantees* that a
* subscription *must* be tied to a component instance, and therefore will
* be cleaned up when the component is unmounted. It is impossible to create
* the subscription and pass it in - this method must be the one to create
* the subscription and therefore can guarantee it is retained in a way that
* will be cleaned up.
*
* @param eventEmitter emitter to subscribe to.
* @param eventType Type of event to listen to.
* @param listener Function to invoke when event occurs.
* @param context Object to use as listener context.
*/
addListenerOn(
eventEmitter: any,
eventType: string,
listener: () => any,
context: any,
): void;
}
interface ScrollResponderMixin extends SubscribableMixin {
/**
* Invoke this from an `onScroll` event.
*/
scrollResponderHandleScrollShouldSetResponder(): boolean;
/**
* Merely touch starting is not sufficient for a scroll view to become the
* responder. Being the "responder" means that the very next touch move/end
* event will result in an action/movement.
*
* Invoke this from an `onStartShouldSetResponder` event.
*
* `onStartShouldSetResponder` is used when the next move/end will trigger
* some UI movement/action, but when you want to yield priority to views
* nested inside of the view.
*
* There may be some cases where scroll views actually should return `true`
* from `onStartShouldSetResponder`: Any time we are detecting a standard tap
* that gives priority to nested views.
*
* - If a single tap on the scroll view triggers an action such as
* recentering a map style view yet wants to give priority to interaction
* views inside (such as dropped pins or labels), then we would return true
* from this method when there is a single touch.
*
* - Similar to the previous case, if a two finger "tap" should trigger a
* zoom, we would check the `touches` count, and if `>= 2`, we would return
* true.
*
*/
scrollResponderHandleStartShouldSetResponder(): boolean;
/**
* There are times when the scroll view wants to become the responder
* (meaning respond to the next immediate `touchStart/touchEnd`), in a way
* that *doesn't* give priority to nested views (hence the capture phase):
*
* - Currently animating.
* - Tapping anywhere that is not the focused input, while the keyboard is
* up (which should dismiss the keyboard).
*
* Invoke this from an `onStartShouldSetResponderCapture` event.
*/
scrollResponderHandleStartShouldSetResponderCapture(
e: ScrollResponderEvent,
): boolean;
/**
* Invoke this from an `onResponderReject` event.
*
* Some other element is not yielding its role as responder. Normally, we'd
* just disable the `UIScrollView`, but a touch has already began on it, the
* `UIScrollView` will not accept being disabled after that. The easiest
* solution for now is to accept the limitation of disallowing this
* altogether. To improve this, find a way to disable the `UIScrollView` after
* a touch has already started.
*/
scrollResponderHandleResponderReject(): any;
/**
* We will allow the scroll view to give up its lock iff it acquired the lock
* during an animation. This is a very useful default that happens to satisfy
* many common user experiences.
*
* - Stop a scroll on the left edge, then turn that into an outer view's
* backswipe.
* - Stop a scroll mid-bounce at the top, continue pulling to have the outer
* view dismiss.
* - However, without catching the scroll view mid-bounce (while it is
* motionless), if you drag far enough for the scroll view to become
* responder (and therefore drag the scroll view a bit), any backswipe
* navigation of a swipe gesture higher in the view hierarchy, should be
* rejected.
*/
scrollResponderHandleTerminationRequest(): boolean;
/**
* Invoke this from an `onTouchEnd` event.
*
* @param e Event.
*/
scrollResponderHandleTouchEnd(e: ScrollResponderEvent): void;
/**
* Invoke this from an `onResponderRelease` event.
*/
scrollResponderHandleResponderRelease(e: ScrollResponderEvent): void;
scrollResponderHandleScroll(e: ScrollResponderEvent): void;
/**
* Invoke this from an `onResponderGrant` event.
*/
scrollResponderHandleResponderGrant(e: ScrollResponderEvent): void;
/**
* Unfortunately, `onScrollBeginDrag` also fires when *stopping* the scroll
* animation, and there's not an easy way to distinguish a drag vs. stopping
* momentum.
*
* Invoke this from an `onScrollBeginDrag` event.
*/
scrollResponderHandleScrollBeginDrag(e: ScrollResponderEvent): void;
/**
* Invoke this from an `onScrollEndDrag` event.
*/
scrollResponderHandleScrollEndDrag(e: ScrollResponderEvent): void;
/**
* Invoke this from an `onMomentumScrollBegin` event.
*/
scrollResponderHandleMomentumScrollBegin(e: ScrollResponderEvent): void;
/**
* Invoke this from an `onMomentumScrollEnd` event.
*/
scrollResponderHandleMomentumScrollEnd(e: ScrollResponderEvent): void;
/**
* Invoke this from an `onTouchStart` event.
*
* Since we know that the `SimpleEventPlugin` occurs later in the plugin
* order, after `ResponderEventPlugin`, we can detect that we were *not*
* permitted to be the responder (presumably because a contained view became
* responder). The `onResponderReject` won't fire in that case - it only
* fires when a *current* responder rejects our request.
*
* @param e Touch Start event.
*/
scrollResponderHandleTouchStart(e: ScrollResponderEvent): void;
/**
* Invoke this from an `onTouchMove` event.
*
* Since we know that the `SimpleEventPlugin` occurs later in the plugin
* order, after `ResponderEventPlugin`, we can detect that we were *not*
* permitted to be the responder (presumably because a contained view became
* responder). The `onResponderReject` won't fire in that case - it only
* fires when a *current* responder rejects our request.
*
* @param e Touch Start event.
*/
scrollResponderHandleTouchMove(e: ScrollResponderEvent): void;
/**
* A helper function for this class that lets us quickly determine if the
* view is currently animating. This is particularly useful to know when
* a touch has just started or ended.
*/
scrollResponderIsAnimating(): boolean;
/**
* Returns the node that represents native view that can be scrolled.
* Components can pass what node to use by defining a `getScrollableNode`
* function otherwise `this` is used.
*/
scrollResponderGetScrollableNode(): any;
/**
* A helper function to scroll to a specific point in the scrollview.
* This is currently used to help focus on child textviews, but can also
* be used to quickly scroll to any element we want to focus. Syntax:
*
* scrollResponderScrollTo(options: {x: number = 0; y: number = 0; animated: boolean = true})
*
* Note: The weird argument signature is due to the fact that, for historical reasons,
* the function also accepts separate arguments as an alternative to the options object.
* This is deprecated due to ambiguity (y before x), and SHOULD NOT BE USED.
*/
scrollResponderScrollTo(
x?:
| number
| {
x?: number | undefined;
y?: number | undefined;
animated?: boolean | undefined;
},
y?: number,
animated?: boolean,
): void;
/**
* A helper function to zoom to a specific rect in the scrollview. The argument has the shape
* {x: number; y: number; width: number; height: number; animated: boolean = true}
*
* @platform ios
*/
scrollResponderZoomTo(
rect: {
x: number;
y: number;
width: number;
height: number;
animated?: boolean | undefined;
},
animated?: boolean, // deprecated, put this inside the rect argument instead
): void;
/**
* This method should be used as the callback to onFocus in a TextInputs'
* parent view. Note that any module using this mixin needs to return
* the parent view's ref in getScrollViewRef() in order to use this method.
* @param nodeHandle The TextInput node handle
* @param additionalOffset The scroll view's top "contentInset".
* Default is 0.
* @param preventNegativeScrolling Whether to allow pulling the content
* down to make it meet the keyboard's top. Default is false.
*/
scrollResponderScrollNativeHandleToKeyboard(
nodeHandle: any,
additionalOffset?: number,
preventNegativeScrollOffset?: boolean,
): void;
/**
* The calculations performed here assume the scroll view takes up the entire
* screen - even if has some content inset. We then measure the offsets of the
* keyboard, and compensate both for the scroll view's "contentInset".
*
* @param left Position of input w.r.t. table view.
* @param top Position of input w.r.t. table view.
* @param width Width of the text input.
* @param height Height of the text input.
*/
scrollResponderInputMeasureAndScrollToKeyboard(
left: number,
top: number,
width: number,
height: number,
): void;
scrollResponderTextInputFocusError(e: ScrollResponderEvent): void;
/**
* `componentWillMount` is the closest thing to a standard "constructor" for
* React components.
*
* The `keyboardWillShow` is called before input focus.
*/
componentWillMount(): void;
/**
* Warning, this may be called several times for a single keyboard opening.
* It's best to store the information in this method and then take any action
* at a later point (either in `keyboardDidShow` or other).
*
* Here's the order that events occur in:
* - focus
* - willShow {startCoordinates, endCoordinates} several times
* - didShow several times
* - blur
* - willHide {startCoordinates, endCoordinates} several times
* - didHide several times
*
* The `ScrollResponder` providesModule callbacks for each of these events.
* Even though any user could have easily listened to keyboard events
* themselves, using these `props` callbacks ensures that ordering of events
* is consistent - and not dependent on the order that the keyboard events are
* subscribed to. This matters when telling the scroll view to scroll to where
* the keyboard is headed - the scroll responder better have been notified of
* the keyboard destination before being instructed to scroll to where the
* keyboard will be. Stick to the `ScrollResponder` callbacks, and everything
* will work.
*
* WARNING: These callbacks will fire even if a keyboard is displayed in a
* different navigation pane. Filter out the events to determine if they are
* relevant to you. (For example, only if you receive these callbacks after
* you had explicitly focused a node etc).
*/
scrollResponderKeyboardWillShow(e: ScrollResponderEvent): void;
scrollResponderKeyboardWillHide(e: ScrollResponderEvent): void;
scrollResponderKeyboardDidShow(e: ScrollResponderEvent): void;
scrollResponderKeyboardDidHide(e: ScrollResponderEvent): void;
}
export interface ScrollViewPropsIOS {
/**
* When true the scroll view bounces horizontally when it reaches the end
* even if the content is smaller than the scroll view itself. The default
* value is true when `horizontal={true}` and false otherwise.
*/
alwaysBounceHorizontal?: boolean | undefined;
/**
* When true the scroll view bounces vertically when it reaches the end
* even if the content is smaller than the scroll view itself. The default
* value is false when `horizontal={true}` and true otherwise.
*/
alwaysBounceVertical?: boolean | undefined;
/**
* Controls whether iOS should automatically adjust the content inset for scroll views that are placed behind a navigation bar or tab bar/ toolbar.
* The default value is true.
*/
automaticallyAdjustContentInsets?: boolean | undefined; // true
/**
* Controls whether the ScrollView should automatically adjust its contentInset and
* scrollViewInsets when the Keyboard changes its size. The default value is false.
*/
automaticallyAdjustKeyboardInsets?: boolean | undefined;
/**
* Controls whether iOS should automatically adjust the scroll indicator
* insets. The default value is true. Available on iOS 13 and later.
*/
automaticallyAdjustsScrollIndicatorInsets?: boolean | undefined;
/**
* When true the scroll view bounces when it reaches the end of the
* content if the content is larger than the scroll view along the axis of
* the scroll direction. When false it disables all bouncing even if
* the `alwaysBounce*` props are true. The default value is true.
*/
bounces?: boolean | undefined;
/**
* When true gestures can drive zoom past min/max and the zoom will animate
* to the min/max value at gesture end otherwise the zoom will not exceed
* the limits.
*/
bouncesZoom?: boolean | undefined;
/**
* When false once tracking starts won't try to drag if the touch moves.
* The default value is true.
*/
canCancelContentTouches?: boolean | undefined;
/**
* When true the scroll view automatically centers the content when the
* content is smaller than the scroll view bounds; when the content is
* larger than the scroll view this property has no effect. The default
* value is false.
*/
centerContent?: boolean | undefined;
/**
* The amount by which the scroll view content is inset from the edges of the scroll view.
* Defaults to {0, 0, 0, 0}.
*/
contentInset?: Insets | undefined; // zeros
/**
* Used to manually set the starting scroll offset.
* The default value is {x: 0, y: 0}
*/
contentOffset?: PointProp | undefined; // zeros
/**
* This property specifies how the safe area insets are used to modify the content area of the scroll view.
* The default value of this property is "never".
*/
contentInsetAdjustmentBehavior?:
| 'automatic'
| 'scrollableAxes'
| 'never'
| 'always'
| undefined;
/**
* When true the ScrollView will try to lock to only vertical or horizontal
* scrolling while dragging. The default value is false.
*/
directionalLockEnabled?: boolean | undefined;
/**
* The style of the scroll indicators.
* - default (the default), same as black.
* - black, scroll indicator is black. This style is good against
* a white content background.
* - white, scroll indicator is white. This style is good against
* a black content background.
*/
indicatorStyle?: 'default' | 'black' | 'white' | undefined;
/**
* When set, the scroll view will adjust the scroll position so that the first child
* that is currently visible and at or beyond minIndexForVisible will not change position.
* This is useful for lists that are loading content in both directions, e.g. a chat thread,
* where new messages coming in might otherwise cause the scroll position to jump. A value
* of 0 is common, but other values such as 1 can be used to skip loading spinners or other
* content that should not maintain position.
*
* The optional autoscrollToTopThreshold can be used to make the content automatically scroll
* to the top after making the adjustment if the user was within the threshold of the top
* before the adjustment was made. This is also useful for chat-like applications where you
* want to see new messages scroll into place, but not if the user has scrolled up a ways and
* it would be disruptive to scroll a bunch.
*
* Caveat 1: Reordering elements in the scrollview with this enabled will probably cause
* jumpiness and jank. It can be fixed, but there are currently no plans to do so. For now,
* don't re-order the content of any ScrollViews or Lists that use this feature.
*
* Caveat 2: This uses contentOffset and frame.origin in native code to compute visibility.
* Occlusion, transforms, and other complexity won't be taken into account as to whether
* content is "visible" or not.
*/
maintainVisibleContentPosition?:
| null
| {
autoscrollToTopThreshold?: number | null | undefined;
minIndexForVisible: number;
}
| undefined;
/**
* The maximum allowed zoom scale. The default value is 1.0.
*/
maximumZoomScale?: number | undefined;
/**
* The minimum allowed zoom scale. The default value is 1.0.
*/
minimumZoomScale?: number | undefined;
/**
* Called when a scrolling animation ends.
*/
onScrollAnimationEnd?: (() => void) | undefined;
/**
* When true, ScrollView allows use of pinch gestures to zoom in and out.
* The default value is true.
*/
pinchGestureEnabled?: boolean | undefined;
/**
* Limits how often scroll events will be fired while scrolling, specified as
* a time interval in ms. This may be useful when expensive work is performed
* in response to scrolling. Values <= `16` will disable throttling,
* regardless of the refresh rate of the device.
*/
scrollEventThrottle?: number | undefined;
/**
* The amount by which the scroll view indicators are inset from the edges of the scroll view.
* This should normally be set to the same value as the contentInset.
* Defaults to {0, 0, 0, 0}.
*/
scrollIndicatorInsets?: Insets | undefined; //zeroes
/**
* When true, the scroll view can be programmatically scrolled beyond its
* content size. The default value is false.
* @platform ios
*/
scrollToOverflowEnabled?: boolean | undefined;
/**
* When true the scroll view scrolls to top when the status bar is tapped.
* The default value is true.
*/
scrollsToTop?: boolean | undefined;
/**
* When `snapToInterval` is set, `snapToAlignment` will define the relationship of the snapping to the scroll view.
* - `start` (the default) will align the snap at the left (horizontal) or top (vertical)
* - `center` will align the snap in the center
* - `end` will align the snap at the right (horizontal) or bottom (vertical)
*/
snapToAlignment?: 'start' | 'center' | 'end' | undefined;
/**
* Fires when the scroll view scrolls to top after the status bar has been tapped
* @platform ios
*/
onScrollToTop?:
| ((event: NativeSyntheticEvent<NativeScrollEvent>) => void)
| undefined;
/**
* The current scale of the scroll view content. The default value is 1.0.
*/
zoomScale?: number | undefined;
}
export interface ScrollViewPropsAndroid {
/**
* Sometimes a scrollview takes up more space than its content fills.
* When this is the case, this prop will fill the rest of the
* scrollview with a color to avoid setting a background and creating
* unnecessary overdraw. This is an advanced optimization that is not
* needed in the general case.
*/
endFillColor?: ColorValue | undefined;
/**
* Tag used to log scroll performance on this scroll view. Will force
* momentum events to be turned on (see sendMomentumEvents). This doesn't do
* anything out of the box and you need to implement a custom native
* FpsListener for it to be useful.
* @platform android
*/
scrollPerfTag?: string | undefined;
/**
* Used to override default value of overScroll mode.
* Possible values:
* - 'auto' - Default value, allow a user to over-scroll this view only if the content is large enough to meaningfully scroll.
* - 'always' - Always allow a user to over-scroll this view.
* - 'never' - Never allow a user to over-scroll this view.
*/
overScrollMode?: 'auto' | 'always' | 'never' | undefined;
/**
* Enables nested scrolling for Android API level 21+. Nested scrolling is supported by default on iOS.
*/
nestedScrollEnabled?: boolean | undefined;
/**
* Controls the fading effect at the edges of the scroll content.
*
* A value greater than 0 will apply the fading effect, indicating more content is available
* to scroll.
*
* You can specify a single number to apply the same fading length to both edges.
* Alternatively, use an object with `start` and `end` properties to set different
* fading lengths for the start and end of the scroll content.
*
* The default value is 0.
*
* @platform android
*/
fadingEdgeLength?: number | {start: number; end: number} | undefined;
/**
* Causes the scrollbars not to turn transparent when they are not in use. The default value is false.
*/
persistentScrollbar?: boolean | undefined;
}
export interface ScrollViewProps
extends ViewProps,
ScrollViewPropsIOS,
ScrollViewPropsAndroid,
Touchable {
/**
* These styles will be applied to the scroll view content container which
* wraps all of the child views. Example:
*
* return (
* <ScrollView contentContainerStyle={styles.contentContainer}>
* </ScrollView>
* );
* ...
* const styles = StyleSheet.create({
* contentContainer: {
* paddingVertical: 20
* }
* });
*/
contentContainerStyle?: StyleProp<ViewStyle> | undefined;
/**
* A ref to the inner View element of the ScrollView. This should be used
* instead of calling `getInnerViewRef`.
*/
innerViewRef?: React.RefObject<View> | undefined;
/**
* A ref to the Native ScrollView component. This ref can be used to call
* all of ScrollView's public methods, in addition to native methods like
* measure, measureLayout, etc.
*/
scrollViewRef?: React.RefObject<ScrollView> | undefined;
/**
* A floating-point number that determines how quickly the scroll view
* decelerates after the user lifts their finger. You may also use string
* shortcuts `"normal"` and `"fast"` which match the underlying iOS settings
* for `UIScrollViewDecelerationRateNormal` and
* `UIScrollViewDecelerationRateFast` respectively.
*
* - `'normal'`: 0.998 on iOS, 0.985 on Android (the default)
* - `'fast'`: 0.99 on iOS, 0.9 on Android
*/
decelerationRate?: 'fast' | 'normal' | number | undefined;
/**
* When true the scroll view's children are arranged horizontally in a row
* instead of vertically in a column. The default value is false.
*/
horizontal?: boolean | null | undefined;
/**
* If sticky headers should stick at the bottom instead of the top of the
* ScrollView. This is usually used with inverted ScrollViews.
*/
invertStickyHeaders?: boolean | undefined;
/**
* Determines whether the keyboard gets dismissed in response to a drag.
* - 'none' (the default) drags do not dismiss the keyboard.
* - 'onDrag' the keyboard is dismissed when a drag begins.
* - 'interactive' the keyboard is dismissed interactively with the drag
* and moves in synchrony with the touch; dragging upwards cancels the
* dismissal.
*/
keyboardDismissMode?: 'none' | 'interactive' | 'on-drag' | undefined;
/**
* Determines when the keyboard should stay visible after a tap.
* - 'never' (the default), tapping outside of the focused text input when the keyboard is up dismisses the keyboard. When this happens, children won't receive the tap.
* - 'always', the keyboard will not dismiss automatically, and the scroll view will not catch taps, but children of the scroll view can catch taps.
* - 'handled', the keyboard will not dismiss automatically when the tap was handled by a children, (or captured by an ancestor).
* - false, deprecated, use 'never' instead
* - true, deprecated, use 'always' instead
*/
keyboardShouldPersistTaps?:
| boolean
| 'always'
| 'never'
| 'handled'
| undefined;
/**
* Called when scrollable content view of the ScrollView changes.
* Handler function is passed the content width and content height as parameters: (contentWidth, contentHeight)
* It's implemented using onLayout handler attached to the content container which this ScrollView renders.
*
*/
onContentSizeChange?:
| ((contentWidth: number, contentHeight: number) => void)
| undefined;
/**
* Fires at most once per frame during scrolling.
*/
onScroll?:
| ((event: NativeSyntheticEvent<NativeScrollEvent>) => void)
| undefined;
/**
* Fires if a user initiates a scroll gesture.
*/
onScrollBeginDrag?:
| ((event: NativeSyntheticEvent<NativeScrollEvent>) => void)
| undefined;
/**
* Fires when a user has finished scrolling.
*/
onScrollEndDrag?:
| ((event: NativeSyntheticEvent<NativeScrollEvent>) => void)
| undefined;
/**
* Fires when scroll view has finished moving
*/
onMomentumScrollEnd?:
| ((event: NativeSyntheticEvent<NativeScrollEvent>) => void)
| undefined;
/**
* Fires when scroll view has begun moving
*/
onMomentumScrollBegin?:
| ((event: NativeSyntheticEvent<NativeScrollEvent>) => void)
| undefined;
/**
* When true the scroll view stops on multiples of the scroll view's size
* when scrolling. This can be used for horizontal pagination. The default
* value is false.
*/
pagingEnabled?: boolean | undefined;
/**
* When false, the content does not scroll. The default value is true
*/
scrollEnabled?: boolean | undefined; // true
/**
* Experimental: When true offscreen child views (whose `overflow` value is
* `hidden`) are removed from their native backing superview when offscreen.
* This can improve scrolling performance on long lists. The default value is
* false.
*/
removeClippedSubviews?: boolean | undefined;
/**
* When true, shows a horizontal scroll indicator.
*/
showsHorizontalScrollIndicator?: boolean | undefined;
/**
* When true, shows a vertical scroll indicator.
*/
showsVerticalScrollIndicator?: boolean | undefined;
/**
* When true, Sticky header is hidden when scrolling down, and dock at the top when scrolling up.
*/
stickyHeaderHiddenOnScroll?: boolean | undefined;
/**
* Style
*/
style?: StyleProp<ViewStyle> | undefined;
/**
* A RefreshControl component, used to provide pull-to-refresh
* functionality for the ScrollView.
*/
refreshControl?: React.ReactElement<RefreshControlProps> | undefined;
/**
* When set, causes the scroll view to stop at multiples of the value of `snapToInterval`.
* This can be used for paginating through children that have lengths smaller than the scroll view.
* Used in combination with `snapToAlignment` and `decelerationRate="fast"`. Overrides less
* configurable `pagingEnabled` prop.
*/
snapToInterval?: number | undefined;
/**
* When set, causes the scroll view to stop at the defined offsets. This can be used for
* paginating through variously sized children that have lengths smaller than the scroll view.
* Typically used in combination with `decelerationRate="fast"`. Overrides less configurable
* `pagingEnabled` and `snapToInterval` props.
*/
snapToOffsets?: number[] | undefined;
/**
* Use in conjunction with `snapToOffsets`. By default, the beginning of the list counts as a
* snap offset. Set `snapToStart` to false to disable this behavior and allow the list to scroll
* freely between its start and the first `snapToOffsets` offset. The default value is true.
*/
snapToStart?: boolean | undefined;
/**
* Use in conjunction with `snapToOffsets`. By default, the end of the list counts as a snap
* offset. Set `snapToEnd` to false to disable this behavior and allow the list to scroll freely
* between its end and the last `snapToOffsets` offset. The default value is true.
*/
snapToEnd?: boolean | undefined;
/**
* An array of child indices determining which children get docked to the
* top of the screen when scrolling. For example passing
* `stickyHeaderIndices={[0]}` will cause the first child to be fixed to the
* top of the scroll view. This property is not supported in conjunction
* with `horizontal={true}`.
*/
stickyHeaderIndices?: number[] | undefined;
/**
* When true, the scroll view stops on the next index (in relation to scroll position at release)
* regardless of how fast the gesture is. This can be used for horizontal pagination when the page
* is less than the width of the ScrollView. The default value is false.
*/
disableIntervalMomentum?: boolean | undefined;
/**
* When true, the default JS pan responder on the ScrollView is disabled, and full control over
* touches inside the ScrollView is left to its child components. This is particularly useful
* if `snapToInterval` is enabled, since it does not follow typical touch patterns. Do not use
* this on regular ScrollView use cases without `snapToInterval` as it may cause unexpected
* touches to occur while scrolling. The default value is false.
*/
disableScrollViewPanResponder?: boolean | undefined;
/**
* A React Component that will be used to render sticky headers, should be used together with
* stickyHeaderIndices. You may need to set this component if your sticky header uses custom
* transforms, for example, when you want your list to have an animated and hidable header.
* If component have not been provided, the default ScrollViewStickyHeader component will be used.
*/
StickyHeaderComponent?: React.ComponentType<any> | undefined;
}
declare class ScrollViewComponent extends React.Component<ScrollViewProps> {}
export declare const ScrollViewBase: Constructor<ScrollResponderMixin> &
typeof ScrollViewComponent;
export class ScrollView extends ScrollViewBase {
/**
* Scrolls to a given x, y offset, either immediately or with a smooth animation.
* Syntax:
*
* scrollTo(options: {x: number = 0; y: number = 0; animated: boolean = true})
*
* Note: The weird argument signature is due to the fact that, for historical reasons,
* the function also accepts separate arguments as an alternative to the options object.
* This is deprecated due to ambiguity (y before x), and SHOULD NOT BE USED.
*/
scrollTo(
y?:
| number
| {
x?: number | undefined;
y?: number | undefined;
animated?: boolean | undefined;
},
deprecatedX?: number,
deprecatedAnimated?: boolean,
): void;
/**
* A helper function that scrolls to the end of the scrollview;
* If this is a vertical ScrollView, it scrolls to the bottom.
* If this is a horizontal ScrollView scrolls to the right.
*
* The options object has an animated prop, that enables the scrolling animation or not.
* The animated prop defaults to true
*/
scrollToEnd(options?: {animated?: boolean | undefined}): void;
/**
* Displays the scroll indicators momentarily.
*/
flashScrollIndicators(): void;
/**
* Returns a reference to the underlying scroll responder, which supports
* operations like `scrollTo`. All ScrollView-like components should
* implement this method so that they can be composed while providing access
* to the underlying scroll responder's methods.
*/
getScrollResponder(): ScrollResponderMixin;
getScrollableNode(): any;
// Undocumented
getInnerViewNode(): any;
/**
* Returns a reference to the underlying native scroll view, or null if the
* native instance is not mounted.
*/
getNativeScrollRef: () => HostInstance | null;
/**
* @deprecated Use scrollTo instead
*/
scrollWithoutAnimationTo?: ((y: number, x: number) => void) | undefined;
/**
* This function sends props straight to native. They will not participate in
* future diff process - this means that if you do not include them in the
* next render, they will remain active (see [Direct
* Manipulation](https://reactnative.dev/docs/the-new-architecture/direct-manipulation-new-architecture)).
*/
setNativeProps(nativeProps: object): void;
}
export interface NativeScrollRectangle {
left: number;
top: number;
bottom: number;
right: number;
}
export interface NativeScrollPoint {
x: number;
y: number;
}
export interface NativeScrollVelocity {
x: number;
y: number;
}
export interface NativeScrollSize {
height: number;
width: number;
}
export interface NativeScrollEvent {
contentInset: NativeScrollRectangle;
contentOffset: NativeScrollPoint;
contentSize: NativeScrollSize;
layoutMeasurement: NativeScrollSize;
velocity?: NativeScrollVelocity | undefined;
zoomScale: number;
/**
* @platform ios
*/
targetContentOffset?: NativeScrollPoint | undefined;
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,52 @@
/**
* 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.
*
* @flow strict-local
* @format
*/
import type {HostComponent} from '../../../src/private/types/HostComponent';
import type {Double} from '../../Types/CodegenTypes';
import codegenNativeCommands from '../../Utilities/codegenNativeCommands';
import * as React from 'react';
type ScrollViewNativeComponentType = HostComponent<{...}>;
interface NativeCommands {
+flashScrollIndicators: (
viewRef: React.ElementRef<ScrollViewNativeComponentType>,
) => void;
+scrollTo: (
viewRef: React.ElementRef<ScrollViewNativeComponentType>,
x: Double,
y: Double,
animated: boolean,
) => void;
+scrollToEnd: (
viewRef: React.ElementRef<ScrollViewNativeComponentType>,
animated: boolean,
) => void;
+zoomToRect: (
viewRef: React.ElementRef<ScrollViewNativeComponentType>,
rect: {
x: Double,
y: Double,
width: Double,
height: Double,
animated?: boolean,
},
animated?: boolean,
) => void;
}
export default (codegenNativeCommands<NativeCommands>({
supportedCommands: [
'flashScrollIndicators',
'scrollTo',
'scrollToEnd',
'zoomToRect',
],
}): NativeCommands);

View File

@@ -0,0 +1,25 @@
/**
* 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.
*
* @flow strict
* @format
*/
import * as React from 'react';
import {createContext} from 'react';
type Value = {horizontal: boolean} | null;
const ScrollViewContext: React.Context<Value> = createContext(null);
if (__DEV__) {
ScrollViewContext.displayName = 'ScrollViewContext';
}
export default ScrollViewContext;
// $FlowFixMe[incompatible-type] frozen objects are readonly
export const HORIZONTAL: Value = Object.freeze({horizontal: true});
// $FlowFixMe[incompatible-type] frozen objects are readonly
export const VERTICAL: Value = Object.freeze({horizontal: false});

View File

@@ -0,0 +1,180 @@
/**
* 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.
*
* @flow strict-local
* @format
*/
import type {HostComponent} from '../../../src/private/types/HostComponent';
import type {PartialViewConfig} from '../../Renderer/shims/ReactNativeTypes';
import type {ScrollViewNativeProps as Props} from './ScrollViewNativeComponentType';
import * as NativeComponentRegistry from '../../NativeComponent/NativeComponentRegistry';
import {ConditionallyIgnoredEventHandlers} from '../../NativeComponent/ViewConfigIgnore';
import Platform from '../../Utilities/Platform';
export const __INTERNAL_VIEW_CONFIG: PartialViewConfig =
Platform.OS === 'android'
? {
uiViewClassName: 'RCTScrollView',
bubblingEventTypes: {},
directEventTypes: {
topMomentumScrollBegin: {
registrationName: 'onMomentumScrollBegin',
},
topMomentumScrollEnd: {
registrationName: 'onMomentumScrollEnd',
},
topScroll: {
registrationName: 'onScroll',
},
topScrollBeginDrag: {
registrationName: 'onScrollBeginDrag',
},
topScrollEndDrag: {
registrationName: 'onScrollEndDrag',
},
},
validAttributes: {
contentOffset: {
diff: require('../../Utilities/differ/pointsDiffer').default,
},
decelerationRate: true,
disableIntervalMomentum: true,
maintainVisibleContentPosition: true,
pagingEnabled: true,
scrollEnabled: true,
showsVerticalScrollIndicator: true,
snapToAlignment: true,
snapToEnd: true,
snapToInterval: true,
snapToOffsets: true,
snapToStart: true,
borderBottomLeftRadius: true,
borderBottomRightRadius: true,
sendMomentumEvents: true,
borderRadius: true,
nestedScrollEnabled: true,
scrollEventThrottle: true,
borderStyle: true,
borderRightColor: {
process: require('../../StyleSheet/processColor').default,
},
borderColor: {
process: require('../../StyleSheet/processColor').default,
},
borderBottomColor: {
process: require('../../StyleSheet/processColor').default,
},
persistentScrollbar: true,
horizontal: true,
endFillColor: {
process: require('../../StyleSheet/processColor').default,
},
fadingEdgeLength: true,
overScrollMode: true,
borderTopLeftRadius: true,
scrollPerfTag: true,
borderTopColor: {
process: require('../../StyleSheet/processColor').default,
},
removeClippedSubviews: true,
borderTopRightRadius: true,
borderLeftColor: {
process: require('../../StyleSheet/processColor').default,
},
pointerEvents: true,
isInvertedVirtualizedList: true,
},
}
: {
uiViewClassName: 'RCTScrollView',
bubblingEventTypes: {},
directEventTypes: {
topMomentumScrollBegin: {
registrationName: 'onMomentumScrollBegin',
},
topMomentumScrollEnd: {
registrationName: 'onMomentumScrollEnd',
},
topScroll: {
registrationName: 'onScroll',
},
topScrollBeginDrag: {
registrationName: 'onScrollBeginDrag',
},
topScrollEndDrag: {
registrationName: 'onScrollEndDrag',
},
topScrollToTop: {
registrationName: 'onScrollToTop',
},
},
validAttributes: {
alwaysBounceHorizontal: true,
alwaysBounceVertical: true,
automaticallyAdjustContentInsets: true,
automaticallyAdjustKeyboardInsets: true,
automaticallyAdjustsScrollIndicatorInsets: true,
bounces: true,
bouncesZoom: true,
canCancelContentTouches: true,
centerContent: true,
contentInset: {
diff: require('../../Utilities/differ/insetsDiffer').default,
},
contentOffset: {
diff: require('../../Utilities/differ/pointsDiffer').default,
},
contentInsetAdjustmentBehavior: true,
decelerationRate: true,
endDraggingSensitivityMultiplier: true,
directionalLockEnabled: true,
disableIntervalMomentum: true,
indicatorStyle: true,
inverted: true,
keyboardDismissMode: true,
maintainVisibleContentPosition: true,
maximumZoomScale: true,
minimumZoomScale: true,
pagingEnabled: true,
pinchGestureEnabled: true,
scrollEnabled: true,
scrollEventThrottle: true,
scrollIndicatorInsets: {
diff: require('../../Utilities/differ/insetsDiffer').default,
},
scrollToOverflowEnabled: true,
scrollsToTop: true,
showsHorizontalScrollIndicator: true,
showsVerticalScrollIndicator: true,
snapToAlignment: true,
snapToEnd: true,
snapToInterval: true,
snapToOffsets: true,
snapToStart: true,
verticalScrollIndicatorInsets: {
diff: require('../../Utilities/differ/insetsDiffer').default,
},
zoomScale: true,
...ConditionallyIgnoredEventHandlers({
onScrollBeginDrag: true,
onMomentumScrollEnd: true,
onScrollEndDrag: true,
onMomentumScrollBegin: true,
onScrollToTop: true,
onScroll: true,
}),
},
};
const ScrollViewNativeComponent: HostComponent<Props> =
NativeComponentRegistry.get<Props>(
'RCTScrollView',
() => __INTERNAL_VIEW_CONFIG,
);
export default ScrollViewNativeComponent;

View File

@@ -0,0 +1,84 @@
/**
* 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.
*
* @flow strict-local
* @format
*/
'use strict';
import type {EdgeInsetsProp} from '../../StyleSheet/EdgeInsetsPropType';
import type {PointProp} from '../../StyleSheet/PointPropType';
import type {ColorValue} from '../../StyleSheet/StyleSheet';
import type {
GestureResponderEvent,
ScrollEvent,
} from '../../Types/CoreEventTypes';
import type {ViewProps} from '../View/ViewPropTypes';
export type ScrollViewNativeProps = $ReadOnly<{
...ViewProps,
alwaysBounceHorizontal?: ?boolean,
alwaysBounceVertical?: ?boolean,
automaticallyAdjustContentInsets?: ?boolean,
automaticallyAdjustKeyboardInsets?: ?boolean,
automaticallyAdjustsScrollIndicatorInsets?: ?boolean,
bounces?: ?boolean,
bouncesZoom?: ?boolean,
canCancelContentTouches?: ?boolean,
centerContent?: ?boolean,
contentInset?: ?EdgeInsetsProp,
contentInsetAdjustmentBehavior?: ?(
| 'automatic'
| 'scrollableAxes'
| 'never'
| 'always'
),
contentOffset?: ?PointProp,
decelerationRate?: ?('fast' | 'normal' | number),
directionalLockEnabled?: ?boolean,
disableIntervalMomentum?: ?boolean,
endFillColor?: ?ColorValue,
fadingEdgeLength?: ?number | {start: number, end: number},
indicatorStyle?: ?('default' | 'black' | 'white'),
isInvertedVirtualizedList?: ?boolean,
keyboardDismissMode?: ?('none' | 'on-drag' | 'interactive'),
maintainVisibleContentPosition?: ?$ReadOnly<{
minIndexForVisible: number,
autoscrollToTopThreshold?: ?number,
}>,
maximumZoomScale?: ?number,
minimumZoomScale?: ?number,
nestedScrollEnabled?: ?boolean,
onMomentumScrollBegin?: ?(event: ScrollEvent) => void,
onMomentumScrollEnd?: ?(event: ScrollEvent) => void,
onScroll?: ?(event: ScrollEvent) => void,
onScrollBeginDrag?: ?(event: ScrollEvent) => void,
onScrollEndDrag?: ?(event: ScrollEvent) => void,
onScrollToTop?: (event: ScrollEvent) => void,
overScrollMode?: ?('auto' | 'always' | 'never'),
pagingEnabled?: ?boolean,
persistentScrollbar?: ?boolean,
pinchGestureEnabled?: ?boolean,
scrollEnabled?: ?boolean,
scrollEventThrottle?: ?number,
scrollIndicatorInsets?: ?EdgeInsetsProp,
scrollPerfTag?: ?string,
scrollToOverflowEnabled?: ?boolean,
scrollsToTop?: ?boolean,
sendMomentumEvents?: ?boolean,
showsHorizontalScrollIndicator?: ?boolean,
showsVerticalScrollIndicator?: ?boolean,
snapToAlignment?: ?('start' | 'center' | 'end'),
snapToEnd?: ?boolean,
snapToInterval?: ?number,
snapToOffsets?: ?$ReadOnlyArray<number>,
snapToStart?: ?boolean,
zoomScale?: ?number,
// Overrides
onResponderGrant?: ?(e: GestureResponderEvent) => void | boolean,
...
}>;

View File

@@ -0,0 +1,322 @@
/**
* 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.
*
* @flow strict-local
* @format
*/
import type {LayoutChangeEvent} from '../../Types/CoreEventTypes';
import Animated from '../../Animated/Animated';
import {isPublicInstance as isFabricPublicInstance} from '../../ReactNative/ReactFabricPublicInstance/ReactFabricPublicInstanceUtils';
import StyleSheet from '../../StyleSheet/StyleSheet';
import Platform from '../../Utilities/Platform';
import useMergeRefs from '../../Utilities/useMergeRefs';
import * as React from 'react';
import {
cloneElement,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
export type ScrollViewStickyHeaderProps = $ReadOnly<{
children?: React.Node,
nextHeaderLayoutY: ?number,
onLayout: (event: LayoutChangeEvent) => void,
scrollAnimatedValue: Animated.Value,
// Will cause sticky headers to stick at the bottom of the ScrollView instead
// of the top.
inverted: ?boolean,
// The height of the parent ScrollView. Currently only set when inverted.
scrollViewHeight: ?number,
nativeID?: ?string,
hiddenOnScroll?: ?boolean,
}>;
interface Instance extends React.ElementRef<typeof Animated.View> {
+setNextHeaderY: number => void;
}
const ScrollViewStickyHeader: component(
ref?: React.RefSetter<Instance>,
...props: ScrollViewStickyHeaderProps
) = function ScrollViewStickyHeader({
ref: forwardedRef,
...props
}: {
ref?: React.RefSetter<Instance>,
...ScrollViewStickyHeaderProps,
}) {
const {
inverted,
scrollViewHeight,
hiddenOnScroll,
scrollAnimatedValue,
nextHeaderLayoutY: _nextHeaderLayoutY,
} = props;
const [measured, setMeasured] = useState<boolean>(false);
const [layoutY, setLayoutY] = useState<number>(0);
const [layoutHeight, setLayoutHeight] = useState<number>(0);
const [translateY, setTranslateY] = useState<?number>(null);
const [nextHeaderLayoutY, setNextHeaderLayoutY] =
useState<?number>(_nextHeaderLayoutY);
const [isFabric, setIsFabric] = useState<boolean>(false);
const callbackRef = useCallback((ref: Instance | null): void => {
if (ref == null) {
return;
}
// $FlowExpectedError[cannot-write]
ref.setNextHeaderY = setNextHeaderLayoutY;
setIsFabric(isFabricPublicInstance(ref));
}, []);
const ref: React.RefSetter<React.ElementRef<typeof Animated.View>> =
// $FlowFixMe[incompatible-type] - Instance is mutated to have `setNextHeaderY`.
useMergeRefs<Instance>(callbackRef, forwardedRef);
const offset = useMemo(
() =>
hiddenOnScroll === true
? Animated.diffClamp(
scrollAnimatedValue
.interpolate({
extrapolateLeft: 'clamp',
inputRange: [layoutY, layoutY + 1],
outputRange: ([0, 1]: Array<number>),
})
.interpolate({
inputRange: [0, 1],
outputRange: ([0, -1]: Array<number>),
}),
-layoutHeight,
0,
)
: null,
[scrollAnimatedValue, layoutHeight, layoutY, hiddenOnScroll],
);
const [animatedTranslateY, setAnimatedTranslateY] = useState<Animated.Node>(
() => {
const inputRange: Array<number> = [-1, 0];
const outputRange: Array<number> = [0, 0];
const initialTranslateY = scrollAnimatedValue.interpolate({
inputRange,
outputRange,
});
if (offset != null) {
return Animated.add(initialTranslateY, offset);
}
return initialTranslateY;
},
);
const haveReceivedInitialZeroTranslateY = useRef<boolean>(true);
const translateYDebounceTimer = useRef<?TimeoutID>(null);
useEffect(() => {
if (translateY !== 0 && translateY != null) {
haveReceivedInitialZeroTranslateY.current = false;
}
}, [translateY]);
// This is called whenever the (Interpolated) Animated Value
// updates, which is several times per frame during scrolling.
// To ensure that the Fabric ShadowTree has the most recent
// translate style of this node, we debounce the value and then
// pass it through to the underlying node during render.
// This is:
// 1. Only an issue in Fabric.
// 2. Worse in Android than iOS. In Android, but not iOS, you
// can touch and move your finger slightly and still trigger
// a "tap" event. In iOS, moving will cancel the tap in
// both Fabric and non-Fabric. On Android when you move
// your finger, the hit-detection moves from the Android
// platform to JS, so we need the ShadowTree to have knowledge
// of the current position.
const animatedValueListener = useCallback(({value}: $FlowFixMe) => {
const debounceTimeout: number = Platform.OS === 'android' ? 15 : 64;
// When the AnimatedInterpolation is recreated, it always initializes
// to a value of zero and emits a value change of 0 to its listeners.
if (value === 0 && !haveReceivedInitialZeroTranslateY.current) {
haveReceivedInitialZeroTranslateY.current = true;
return;
}
if (translateYDebounceTimer.current != null) {
clearTimeout(translateYDebounceTimer.current);
}
translateYDebounceTimer.current = setTimeout(
() => setTranslateY(value),
debounceTimeout,
);
}, []);
useEffect(() => {
const inputRange: Array<number> = [-1, 0];
const outputRange: Array<number> = [0, 0];
if (measured) {
if (inverted === true) {
// The interpolation looks like:
// - Negative scroll: no translation
// - `stickStartPoint` is the point at which the header will start sticking.
// It is calculated using the ScrollView viewport height so it is a the bottom.
// - Headers that are in the initial viewport will never stick, `stickStartPoint`
// will be negative.
// - From 0 to `stickStartPoint` no translation. This will cause the header
// to scroll normally until it reaches the top of the scroll view.
// - From `stickStartPoint` to when the next header y hits the bottom edge of the header: translate
// equally to scroll. This will cause the header to stay at the top of the scroll view.
// - Past the collision with the next header y: no more translation. This will cause the
// header to continue scrolling up and make room for the next sticky header.
// In the case that there is no next header just translate equally to
// scroll indefinitely.
if (scrollViewHeight != null) {
const stickStartPoint = layoutY + layoutHeight - scrollViewHeight;
if (stickStartPoint > 0) {
inputRange.push(stickStartPoint);
outputRange.push(0);
inputRange.push(stickStartPoint + 1);
outputRange.push(1);
// If the next sticky header has not loaded yet (probably windowing) or is the last
// we can just keep it sticked forever.
const collisionPoint =
(nextHeaderLayoutY || 0) - layoutHeight - scrollViewHeight;
if (collisionPoint > stickStartPoint) {
inputRange.push(collisionPoint, collisionPoint + 1);
outputRange.push(
collisionPoint - stickStartPoint,
collisionPoint - stickStartPoint,
);
}
}
}
} else {
// The interpolation looks like:
// - Negative scroll: no translation
// - From 0 to the y of the header: no translation. This will cause the header
// to scroll normally until it reaches the top of the scroll view.
// - From header y to when the next header y hits the bottom edge of the header: translate
// equally to scroll. This will cause the header to stay at the top of the scroll view.
// - Past the collision with the next header y: no more translation. This will cause the
// header to continue scrolling up and make room for the next sticky header.
// In the case that there is no next header just translate equally to
// scroll indefinitely.
inputRange.push(layoutY);
outputRange.push(0);
// If the next sticky header has not loaded yet (probably windowing) or is the last
// we can just keep it sticked forever.
const collisionPoint = (nextHeaderLayoutY || 0) - layoutHeight;
if (collisionPoint >= layoutY) {
inputRange.push(collisionPoint, collisionPoint + 1);
outputRange.push(collisionPoint - layoutY, collisionPoint - layoutY);
} else {
inputRange.push(layoutY + 1);
outputRange.push(1);
}
}
}
let newAnimatedTranslateY: Animated.Node = scrollAnimatedValue.interpolate({
inputRange,
outputRange,
});
if (offset != null) {
newAnimatedTranslateY = Animated.add(newAnimatedTranslateY, offset);
}
// add the event listener
let animatedListenerId;
if (isFabric) {
animatedListenerId = newAnimatedTranslateY.addListener(
animatedValueListener,
);
}
setAnimatedTranslateY(newAnimatedTranslateY);
// clean up the event listener and timer
return () => {
if (animatedListenerId) {
newAnimatedTranslateY.removeListener(animatedListenerId);
}
if (translateYDebounceTimer.current != null) {
clearTimeout(translateYDebounceTimer.current);
}
};
}, [
nextHeaderLayoutY,
measured,
layoutHeight,
layoutY,
scrollViewHeight,
scrollAnimatedValue,
inverted,
offset,
animatedValueListener,
isFabric,
]);
const _onLayout = (event: LayoutChangeEvent) => {
setLayoutY(event.nativeEvent.layout.y);
setLayoutHeight(event.nativeEvent.layout.height);
setMeasured(true);
props.onLayout(event);
const child = React.Children.only<$FlowFixMe>(props.children);
if (child.props.onLayout) {
child.props.onLayout(event);
}
};
const child = React.Children.only<$FlowFixMe>(props.children);
const passthroughAnimatedPropExplicitValues =
isFabric && translateY != null
? {
style: {transform: [{translateY}]},
}
: null;
return (
<Animated.View
collapsable={false}
nativeID={props.nativeID}
onLayout={_onLayout}
/* $FlowFixMe[prop-missing] passthroughAnimatedPropExplicitValues isn't properly
included in the Animated.View flow type. */
ref={ref}
style={[
child.props.style,
styles.header,
{transform: [{translateY: animatedTranslateY}]},
]}
passthroughAnimatedPropExplicitValues={
passthroughAnimatedPropExplicitValues
}>
{cloneElement(child, {
onLayout: undefined, // we call this manually through our this._onLayout
style: styles.fill, // We transfer the child style to the wrapper.
})}
</Animated.View>
);
};
const styles = StyleSheet.create({
fill: {
flex: 1,
},
header: {
zIndex: 10,
},
});
export default ScrollViewStickyHeader;

View File

@@ -0,0 +1,30 @@
/**
* 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.
*
* @flow strict-local
* @format
*/
import Platform from '../../Utilities/Platform';
function processDecelerationRate(
decelerationRate: number | 'normal' | 'fast',
): number {
if (decelerationRate === 'normal') {
return Platform.select({
ios: 0.998,
android: 0.985,
});
} else if (decelerationRate === 'fast') {
return Platform.select({
ios: 0.99,
android: 0.9,
});
}
return decelerationRate;
}
export default processDecelerationRate;

View File

@@ -0,0 +1,12 @@
/**
* 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.
*
* @flow strict
* @format
*/
export * from '../../../src/private/specs_DEPRECATED/modules/NativeSoundManager';
export {default} from '../../../src/private/specs_DEPRECATED/modules/NativeSoundManager';

View File

@@ -0,0 +1,21 @@
/**
* 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.
*
* @flow strict
* @format
*/
import NativeSoundManager from './NativeSoundManager';
const SoundManager = {
playTouchSound: function (): void {
if (NativeSoundManager) {
NativeSoundManager.playTouchSound();
}
},
};
export default SoundManager;

View File

@@ -0,0 +1,37 @@
/**
* 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.
*
* @flow strict-local
* @format
*/
'use strict';
import * as React from 'react';
type Props = $ReadOnly<{
/**
* Indicates whether the render function needs to be called again
*/
shouldUpdate: boolean,
/**
* () => renderable
* A function that returns a renderable component
*/
render: () => React.Node,
}>;
class StaticRenderer extends React.Component<Props> {
shouldComponentUpdate(nextProps: Props): boolean {
return nextProps.shouldUpdate;
}
render(): React.Node {
return this.props.render();
}
}
export default StaticRenderer;

View File

@@ -0,0 +1,12 @@
/**
* 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.
*
* @flow strict
* @format
*/
export * from '../../../src/private/specs_DEPRECATED/modules/NativeStatusBarManagerAndroid';
export {default} from '../../../src/private/specs_DEPRECATED/modules/NativeStatusBarManagerAndroid';

View File

@@ -0,0 +1,12 @@
/**
* 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.
*
* @flow strict
* @format
*/
export * from '../../../src/private/specs_DEPRECATED/modules/NativeStatusBarManagerIOS';
export {default} from '../../../src/private/specs_DEPRECATED/modules/NativeStatusBarManagerIOS';

View File

@@ -0,0 +1,142 @@
/**
* 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
*/
import type * as React from 'react';
import {ColorValue} from '../../StyleSheet/StyleSheet';
export type StatusBarStyle = 'default' | 'light-content' | 'dark-content';
export type StatusBarAnimation = 'none' | 'fade' | 'slide';
export interface StatusBarPropsIOS {
/**
* If the network activity indicator should be visible.
*
* @platform ios
*/
networkActivityIndicatorVisible?: boolean | undefined;
/**
* The transition effect when showing and hiding the status bar using
* the hidden prop. Defaults to 'fade'.
*
* @platform ios
*/
showHideTransition?: null | 'fade' | 'slide' | 'none' | undefined;
}
export interface StatusBarPropsAndroid {
/**
* The background color of the status bar.
*
* Please note that this prop has no effect on Android 15+
*
* @platform android
*/
backgroundColor?: ColorValue | undefined;
/**
* If the status bar is translucent. When translucent is set to true,
* the app will draw under the status bar. This is useful when using a
* semi transparent status bar color.
*
* Please note that this prop has no effect on Android 15+
*
* @platform android
*/
translucent?: boolean | undefined;
}
export interface StatusBarProps
extends StatusBarPropsIOS,
StatusBarPropsAndroid {
/**
* If the transition between status bar property changes should be
* animated. Supported for backgroundColor, barStyle and hidden.
*/
animated?: boolean | undefined;
/**
* Sets the color of the status bar text.
*/
barStyle?: null | StatusBarStyle | undefined;
/**
* If the status bar is hidden.
*/
hidden?: boolean | undefined;
}
export class StatusBar extends React.Component<StatusBarProps> {
/**
* The current height of the status bar on the device.
* @platform android
*/
static currentHeight?: number | undefined;
/**
* Show or hide the status bar
* @param hidden The dialog's title.
* @param animation Optional animation when
* changing the status bar hidden property.
*/
static setHidden: (hidden: boolean, animation?: StatusBarAnimation) => void;
/**
* Set the status bar style
* @param style Status bar style to set
* @param animated Animate the style change.
*/
static setBarStyle: (style: StatusBarStyle, animated?: boolean) => void;
/**
* Control the visibility of the network activity indicator
* @param visible Show the indicator.
*/
static setNetworkActivityIndicatorVisible: (visible: boolean) => void;
/**
* Set the background color for the status bar
* @param color Background color.
* @param animated Animate the style change.
*/
static setBackgroundColor: (color: ColorValue, animated?: boolean) => void;
/**
* Control the translucency of the status bar
* @param translucent Set as translucent.
*/
static setTranslucent: (translucent: boolean) => void;
/**
* Push a StatusBar entry onto the stack.
* The return value should be passed to `popStackEntry` when complete.
*
* @param props Object containing the StatusBar props to use in the stack entry.
*/
static pushStackEntry: (props: StatusBarProps) => StatusBarProps;
/**
* Pop a StatusBar entry from the stack.
*
* @param entry Entry returned from `pushStackEntry`.
*/
static popStackEntry: (entry: StatusBarProps) => void;
/**
* Replace an existing StatusBar stack entry with new props.
*
* @param entry Entry returned from `pushStackEntry` to replace.
* @param props Object containing the StatusBar props to use in the replacement stack entry.
*/
static replaceStackEntry: (
entry: StatusBarProps,
props: StatusBarProps,
) => StatusBarProps;
}

View File

@@ -0,0 +1,512 @@
/**
* 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.
*
* @flow
* @format
*/
import type {ColorValue} from '../../StyleSheet/StyleSheet';
import processColor from '../../StyleSheet/processColor';
import Platform from '../../Utilities/Platform';
import NativeStatusBarManagerAndroid from './NativeStatusBarManagerAndroid';
import NativeStatusBarManagerIOS from './NativeStatusBarManagerIOS';
import invariant from 'invariant';
import * as React from 'react';
/**
* Status bar style
*/
export type StatusBarStyle = $Keys<{
/**
* Default status bar style (dark for iOS, light for Android)
*/
default: string,
/**
* Dark background, white texts and icons
*/
'light-content': string,
/**
* Light background, dark texts and icons
*/
'dark-content': string,
...
}>;
/**
* Status bar animation
*/
export type StatusBarAnimation = $Keys<{
/**
* No animation
*/
none: string,
/**
* Fade animation
*/
fade: string,
/**
* Slide animation
*/
slide: string,
...
}>;
export type StatusBarPropsAndroid = $ReadOnly<{
/**
* The background color of the status bar.
*
* Please note that this prop has no effect on Android 15+
*
* @platform android
*/
backgroundColor?: ?ColorValue,
/**
* If the status bar is translucent.
* When translucent is set to true, the app will draw under the status bar.
* This is useful when using a semi transparent status bar color.
*
* Please note that this prop has no effect on Android 15+
*
* @platform android
*/
translucent?: ?boolean,
}>;
export type StatusBarPropsIOS = $ReadOnly<{
/**
* If the network activity indicator should be visible.
*
* @platform ios
*/
networkActivityIndicatorVisible?: ?boolean,
/**
* The transition effect when showing and hiding the status bar using the `hidden`
* prop. Defaults to 'fade'.
*
* @platform ios
*/
showHideTransition?: ?('fade' | 'slide' | 'none'),
}>;
type StatusBarBaseProps = $ReadOnly<{
/**
* If the status bar is hidden.
*/
hidden?: ?boolean,
/**
* If the transition between status bar property changes should be animated.
* Supported for backgroundColor, barStyle and hidden.
*/
animated?: ?boolean,
/**
* Sets the color of the status bar text.
*/
barStyle?: ?('default' | 'light-content' | 'dark-content'),
}>;
export type StatusBarProps = $ReadOnly<{
...StatusBarPropsAndroid,
...StatusBarPropsIOS,
...StatusBarBaseProps,
}>;
type StackProps = {
backgroundColor: ?{
value: StatusBarProps['backgroundColor'],
animated: boolean,
},
barStyle: ?{
value: StatusBarProps['barStyle'],
animated: boolean,
},
translucent: StatusBarProps['translucent'],
hidden: ?{
value: boolean,
animated: boolean,
transition: StatusBarProps['showHideTransition'],
},
networkActivityIndicatorVisible: StatusBarProps['networkActivityIndicatorVisible'],
};
/**
* Merges the prop stack with the default values.
*/
function mergePropsStack(
propsStack: Array<Object>,
defaultValues: Object,
): Object {
return propsStack.reduce(
(prev, cur) => {
for (const prop in cur) {
if (cur[prop] != null) {
prev[prop] = cur[prop];
}
}
return prev;
},
{...defaultValues},
);
}
/**
* Returns an object to insert in the props stack from the props
* and the transition/animation info.
*/
function createStackEntry(props: StatusBarProps): StackProps {
const animated = props.animated ?? false;
const showHideTransition = props.showHideTransition ?? 'fade';
return {
backgroundColor:
props.backgroundColor != null
? {
value: props.backgroundColor,
animated,
}
: null,
barStyle:
props.barStyle != null
? {
value: props.barStyle,
animated,
}
: null,
translucent: props.translucent,
hidden:
props.hidden != null
? {
value: props.hidden,
animated,
transition: showHideTransition,
}
: null,
networkActivityIndicatorVisible: props.networkActivityIndicatorVisible,
};
}
/**
* Component to control the app status bar.
*
* It is possible to have multiple `StatusBar` components mounted at the same
* time. The props will be merged in the order the `StatusBar` components were
* mounted.
*
* ### Imperative API
*
* For cases where using a component is not ideal, there are static methods
* to manipulate the `StatusBar` display stack. These methods have the same
* behavior as mounting and unmounting a `StatusBar` component.
*
* For example, you can call `StatusBar.pushStackEntry` to update the status bar
* before launching a third-party native UI component, and then call
* `StatusBar.popStackEntry` when completed.
*
* ```
* const openThirdPartyBugReporter = async () => {
* // The bug reporter has a dark background, so we push a new status bar style.
* const stackEntry = StatusBar.pushStackEntry({barStyle: 'light-content'});
*
* // `open` returns a promise that resolves when the UI is dismissed.
* await BugReporter.open();
*
* // Don't forget to call `popStackEntry` when you're done.
* StatusBar.popStackEntry(stackEntry);
* };
* ```
*
* There is a legacy imperative API that enables you to manually update the
* status bar styles. However, the legacy API does not update the internal
* `StatusBar` display stack, which means that any changes will be overridden
* whenever a `StatusBar` component is mounted or unmounted.
*
* It is strongly advised that you use `pushStackEntry`, `popStackEntry`, or
* `replaceStackEntry` instead of the static methods beginning with `set`.
*
* ### Constants
*
* `currentHeight` (Android only) The height of the status bar.
*/
class StatusBar extends React.Component<StatusBarProps> {
static _propsStack: Array<StackProps> = [];
static _defaultProps: any = createStackEntry({
backgroundColor:
Platform.OS === 'android'
? (NativeStatusBarManagerAndroid.getConstants()
.DEFAULT_BACKGROUND_COLOR ?? 'black')
: 'black',
barStyle: 'default',
translucent: false,
hidden: false,
networkActivityIndicatorVisible: false,
});
// Timer for updating the native module values at the end of the frame.
static _updateImmediate: ?number = null;
// The current merged values from the props stack.
static _currentValues: ?StackProps = null;
// TODO(janic): Provide a real API to deal with status bar height. See the
// discussion in #6195.
/**
* The current height of the status bar on the device.
*
* @platform android
*/
static currentHeight: ?number =
Platform.OS === 'android'
? NativeStatusBarManagerAndroid.getConstants().HEIGHT
: null;
// Provide an imperative API as static functions of the component.
// See the corresponding prop for more detail.
/**
* Show or hide the status bar
* @param hidden Hide the status bar.
* @param animation Optional animation when
* changing the status bar hidden property.
*/
static setHidden(hidden: boolean, animation?: StatusBarAnimation) {
animation = animation || 'none';
StatusBar._defaultProps.hidden.value = hidden;
if (Platform.OS === 'ios') {
NativeStatusBarManagerIOS.setHidden(hidden, animation);
} else if (Platform.OS === 'android') {
NativeStatusBarManagerAndroid.setHidden(hidden);
}
}
/**
* Set the status bar style
* @param style Status bar style to set
* @param animated Animate the style change.
*/
static setBarStyle(style: StatusBarStyle, animated?: boolean) {
animated = animated || false;
StatusBar._defaultProps.barStyle.value = style;
if (Platform.OS === 'ios') {
NativeStatusBarManagerIOS.setStyle(style, animated);
} else if (Platform.OS === 'android') {
NativeStatusBarManagerAndroid.setStyle(style);
}
}
/**
* DEPRECATED - The status bar network activity indicator is not supported in iOS 13 and later. This will be removed in a future release.
* @param visible Show the indicator.
*
* @deprecated
*/
static setNetworkActivityIndicatorVisible(visible: boolean) {
if (Platform.OS !== 'ios') {
console.warn(
'`setNetworkActivityIndicatorVisible` is only available on iOS',
);
return;
}
StatusBar._defaultProps.networkActivityIndicatorVisible = visible;
NativeStatusBarManagerIOS.setNetworkActivityIndicatorVisible(visible);
}
/**
* Set the background color for the status bar
* @param color Background color.
* @param animated Animate the style change.
*/
static setBackgroundColor(color: ColorValue, animated?: boolean): void {
if (Platform.OS !== 'android') {
console.warn('`setBackgroundColor` is only available on Android');
return;
}
animated = animated || false;
StatusBar._defaultProps.backgroundColor.value = color;
const processedColor = processColor(color);
if (processedColor == null) {
console.warn(
`\`StatusBar.setBackgroundColor\`: Color ${String(color)} parsed to null or undefined`,
);
return;
}
invariant(
typeof processedColor === 'number',
'Unexpected color given for StatusBar.setBackgroundColor',
);
NativeStatusBarManagerAndroid.setColor(processedColor, animated);
}
/**
* Control the translucency of the status bar
* @param translucent Set as translucent.
*/
static setTranslucent(translucent: boolean) {
if (Platform.OS !== 'android') {
console.warn('`setTranslucent` is only available on Android');
return;
}
StatusBar._defaultProps.translucent = translucent;
NativeStatusBarManagerAndroid.setTranslucent(translucent);
}
/**
* Push a StatusBar entry onto the stack.
* The return value should be passed to `popStackEntry` when complete.
*
* @param props Object containing the StatusBar props to use in the stack entry.
*/
static pushStackEntry(props: StatusBarProps): StackProps {
const entry = createStackEntry(props);
StatusBar._propsStack.push(entry);
StatusBar._updatePropsStack();
return entry;
}
/**
* Pop a StatusBar entry from the stack.
*
* @param entry Entry returned from `pushStackEntry`.
*/
static popStackEntry(entry: StackProps) {
const index = StatusBar._propsStack.indexOf(entry);
if (index !== -1) {
StatusBar._propsStack.splice(index, 1);
}
StatusBar._updatePropsStack();
}
/**
* Replace an existing StatusBar stack entry with new props.
*
* @param entry Entry returned from `pushStackEntry` to replace.
* @param props Object containing the StatusBar props to use in the replacement stack entry.
*/
static replaceStackEntry(
entry: StackProps,
props: StatusBarProps,
): StackProps {
const newEntry = createStackEntry(props);
const index = StatusBar._propsStack.indexOf(entry);
if (index !== -1) {
StatusBar._propsStack[index] = newEntry;
}
StatusBar._updatePropsStack();
return newEntry;
}
_stackEntry: ?StackProps = null;
componentDidMount() {
// Every time a StatusBar component is mounted, we push it's prop to a stack
// and always update the native status bar with the props from the top of then
// stack. This allows having multiple StatusBar components and the one that is
// added last or is deeper in the view hierarchy will have priority.
this._stackEntry = StatusBar.pushStackEntry(this.props);
}
componentWillUnmount() {
// When a StatusBar is unmounted, remove itself from the stack and update
// the native bar with the next props.
if (this._stackEntry != null) {
StatusBar.popStackEntry(this._stackEntry);
}
}
componentDidUpdate() {
if (this._stackEntry != null) {
this._stackEntry = StatusBar.replaceStackEntry(
this._stackEntry,
this.props,
);
}
}
/**
* Updates the native status bar with the props from the stack.
*/
static _updatePropsStack = () => {
// Send the update to the native module only once at the end of the frame.
clearImmediate(StatusBar._updateImmediate);
StatusBar._updateImmediate = setImmediate(() => {
const oldProps = StatusBar._currentValues;
const mergedProps = mergePropsStack(
StatusBar._propsStack,
StatusBar._defaultProps,
);
// Update the props that have changed using the merged values from the props stack.
if (Platform.OS === 'ios') {
if (
!oldProps ||
oldProps.barStyle?.value !== mergedProps.barStyle.value
) {
NativeStatusBarManagerIOS.setStyle(
mergedProps.barStyle.value,
mergedProps.barStyle.animated || false,
);
}
if (!oldProps || oldProps.hidden?.value !== mergedProps.hidden.value) {
NativeStatusBarManagerIOS.setHidden(
mergedProps.hidden.value,
mergedProps.hidden.animated
? mergedProps.hidden.transition
: 'none',
);
}
if (
!oldProps ||
oldProps.networkActivityIndicatorVisible !==
mergedProps.networkActivityIndicatorVisible
) {
NativeStatusBarManagerIOS.setNetworkActivityIndicatorVisible(
mergedProps.networkActivityIndicatorVisible,
);
}
} else if (Platform.OS === 'android') {
//todo(T60684787): Add back optimization to only update bar style and
//background color if the new value is different from the old value.
NativeStatusBarManagerAndroid.setStyle(mergedProps.barStyle.value);
const processedColor = processColor(mergedProps.backgroundColor.value);
if (processedColor == null) {
console.warn(
`\`StatusBar._updatePropsStack\`: Color ${mergedProps.backgroundColor.value} parsed to null or undefined`,
);
} else {
invariant(
typeof processedColor === 'number',
'Unexpected color given in StatusBar._updatePropsStack',
);
NativeStatusBarManagerAndroid.setColor(
processedColor,
mergedProps.backgroundColor.animated,
);
}
if (!oldProps || oldProps.hidden?.value !== mergedProps.hidden.value) {
NativeStatusBarManagerAndroid.setHidden(mergedProps.hidden.value);
}
// Activities are not translucent by default, so always set if true.
if (
!oldProps ||
oldProps.translucent !== mergedProps.translucent ||
mergedProps.translucent
) {
NativeStatusBarManagerAndroid.setTranslucent(mergedProps.translucent);
}
}
// Update the current prop values.
StatusBar._currentValues = mergedProps;
});
};
render(): React.Node {
return null;
}
}
export default StatusBar;

View File

@@ -0,0 +1,12 @@
/**
* 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.
*
* @flow strict-local
* @format
*/
export * from '../../../src/private/specs_DEPRECATED/components/AndroidSwitchNativeComponent';
export {default} from '../../../src/private/specs_DEPRECATED/components/AndroidSwitchNativeComponent';

View File

@@ -0,0 +1,118 @@
/**
* 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
*/
import type * as React from 'react';
import {Constructor} from '../../../types/private/Utilities';
import {HostInstance} from '../../../types/public/ReactNativeTypes';
import {ColorValue, StyleProp} from '../../StyleSheet/StyleSheet';
import {ViewStyle} from '../../StyleSheet/StyleSheetTypes';
import {ViewProps} from '../View/ViewPropTypes';
import {NativeSyntheticEvent, TargetedEvent} from '../../Types/CoreEventTypes';
export interface SwitchPropsIOS extends ViewProps {
/**
* Background color when the switch is turned on.
*
* @deprecated use trackColor instead
*/
onTintColor?: ColorValue | undefined;
/**
* Color of the foreground switch grip.
*
* @deprecated use thumbColor instead
*/
thumbTintColor?: ColorValue | undefined;
/**
* Background color when the switch is turned off.
*
* @deprecated use trackColor instead
*/
tintColor?: ColorValue | undefined;
}
/**
* @deprecated Use `SwitchChangeEvent` instead.
*/
export interface SwitchChangeEventData extends TargetedEvent {
value: boolean;
}
export interface SwitchChangeEvent
extends NativeSyntheticEvent<SwitchChangeEventData> {}
export interface SwitchProps extends SwitchPropsIOS {
/**
* Color of the foreground switch grip.
*/
thumbColor?: ColorValue | undefined;
/**
* Custom colors for the switch track
*
* Color when false and color when true
*/
trackColor?:
| {
false?: ColorValue | null | undefined;
true?: ColorValue | null | undefined;
}
| undefined;
/**
* If true the user won't be able to toggle the switch.
* Default value is false.
*/
disabled?: boolean | undefined;
/**
* Invoked with the change event as an argument when the value changes.
*/
onChange?:
| ((event: SwitchChangeEvent) => Promise<void> | void)
| null
| undefined;
/**
* Invoked with the new value when the value changes.
*/
onValueChange?: ((value: boolean) => Promise<void> | void) | null | undefined;
/**
* Used to locate this view in end-to-end tests.
*/
testID?: string | undefined;
/**
* The value of the switch. If true the switch will be turned on.
* Default value is false.
*/
value?: boolean | undefined;
/**
* On iOS, custom color for the background.
* Can be seen when the switch value is false or when the switch is disabled.
*/
ios_backgroundColor?: ColorValue | undefined;
style?: StyleProp<ViewStyle> | undefined;
}
/**
* Renders a boolean input.
*
* This is a controlled component that requires an `onValueChange` callback that
* updates the `value` prop in order for the component to reflect user actions.
* If the `value` prop is not updated, the component will continue to render
* the supplied `value` prop instead of the expected result of any user actions.
*/
declare class SwitchComponent extends React.Component<SwitchProps> {}
declare const SwitchBase: Constructor<HostInstance> & typeof SwitchComponent;
export class Switch extends SwitchBase {}

View File

@@ -0,0 +1,297 @@
/**
* 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.
*
* @flow strict-local
* @format
*/
import type {ColorValue} from '../../StyleSheet/StyleSheet';
import type {NativeSyntheticEvent} from '../../Types/CoreEventTypes';
import type {AccessibilityState} from '../View/ViewAccessibility';
import type {ViewProps} from '../View/ViewPropTypes';
import StyleSheet from '../../StyleSheet/StyleSheet';
import Platform from '../../Utilities/Platform';
import useMergeRefs from '../../Utilities/useMergeRefs';
import AndroidSwitchNativeComponent, {
Commands as AndroidSwitchCommands,
} from './AndroidSwitchNativeComponent';
import SwitchNativeComponent, {
Commands as SwitchCommands,
} from './SwitchNativeComponent';
import * as React from 'react';
import {useLayoutEffect, useRef, useState} from 'react';
export type SwitchPropsIOS = {
/**
* Background color when the switch is turned on.
*
* @deprecated use trackColor instead
*/
onTintColor?: ?ColorValue,
/**
* Color of the foreground switch grip.
*
* @deprecated use thumbColor instead
*/
thumbTintColor?: ?ColorValue,
/**
* Background color when the switch is turned off.
*
* @deprecated use trackColor instead
*/
tintColor?: ?ColorValue,
};
type SwitchChangeEventData = $ReadOnly<{
target: number,
value: boolean,
}>;
export type SwitchChangeEvent = NativeSyntheticEvent<SwitchChangeEventData>;
type SwitchPropsBase = {
/**
If true the user won't be able to toggle the switch.
@default false
*/
disabled?: ?boolean,
/**
The value of the switch. If true the switch will be turned on.
@default false
*/
value?: ?boolean,
/**
Color of the foreground switch grip. If this is set on iOS, the switch grip will lose its drop shadow.
*/
thumbColor?: ?ColorValue,
/**
Custom colors for the switch track.
_iOS_: When the switch value is false, the track shrinks into the border. If you want to change the
color of the background exposed by the shrunken track, use
[`ios_backgroundColor`](https://reactnative.dev/docs/switch#ios_backgroundColor).
*/
trackColor?: ?$ReadOnly<{
false?: ?ColorValue,
true?: ?ColorValue,
}>,
/**
On iOS, custom color for the background. This background color can be
seen either when the switch value is false or when the switch is
disabled (and the switch is translucent).
*/
ios_backgroundColor?: ?ColorValue,
/**
Invoked when the user tries to change the value of the switch. Receives
the change event as an argument. If you want to only receive the new
value, use `onValueChange` instead.
*/
onChange?: ?(event: SwitchChangeEvent) => Promise<void> | void,
/**
Invoked when the user tries to change the value of the switch. Receives
the new value as an argument. If you want to instead receive an event,
use `onChange`.
*/
onValueChange?: ?(value: boolean) => Promise<void> | void,
};
export type SwitchProps = $ReadOnly<{
...ViewProps,
...SwitchPropsIOS,
...SwitchPropsBase,
}>;
const returnsFalse = () => false;
const returnsTrue = () => true;
type SwitchRef = React.ElementRef<
typeof SwitchNativeComponent | typeof AndroidSwitchNativeComponent,
>;
/**
Renders a boolean input.
This is a controlled component that requires an `onValueChange`
callback that updates the `value` prop in order for the component to
reflect user actions. If the `value` prop is not updated, the
component will continue to render the supplied `value` prop instead of
the expected result of any user actions.
```SnackPlayer name=Switch
import React, { useState } from "react";
import { View, Switch, StyleSheet } from "react-native";
const App = () => {
const [isEnabled, setIsEnabled] = useState(false);
const toggleSwitch = () => setIsEnabled(previousState => !previousState);
return (
<View style={styles.container}>
<Switch
trackColor={{ false: "#767577", true: "#81b0ff" }}
thumbColor={isEnabled ? "#f5dd4b" : "#f4f3f4"}
ios_backgroundColor="#3e3e3e"
onValueChange={toggleSwitch}
value={isEnabled}
/>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: "center",
justifyContent: "center"
}
});
export default App;
```
*/
const Switch: component(
ref?: React.RefSetter<SwitchRef>,
...props: SwitchProps
) = function Switch({
ref: forwardedRef,
...props
}: {
ref?: React.RefSetter<SwitchRef>,
...SwitchProps,
}): React.Node {
const {
disabled,
ios_backgroundColor,
onChange,
onValueChange,
style,
thumbColor,
trackColor,
value,
...restProps
} = props;
const trackColorForFalse = trackColor?.false;
const trackColorForTrue = trackColor?.true;
const nativeSwitchRef = useRef<React.ElementRef<
typeof SwitchNativeComponent | typeof AndroidSwitchNativeComponent,
> | null>(null);
const ref = useMergeRefs(nativeSwitchRef, forwardedRef);
// We wrap the native state in an object to force the layout-effect
// below to re-run whenever we get an update from native, even if it's
// not different from the previous native state.
const [native, setNative] = useState({value: (null: ?boolean)});
const handleChange = (event: SwitchChangeEvent) => {
// $FlowFixMe[unused-promise]
onChange?.(event);
// $FlowFixMe[unused-promise]
onValueChange?.(event.nativeEvent.value);
setNative({value: event.nativeEvent.value});
};
useLayoutEffect(() => {
// This is necessary in case native updates the switch and JS decides
// that the update should be ignored and we should stick with the value
// that we have in JS.
const jsValue = value === true;
const shouldUpdateNativeSwitch =
native.value != null && native.value !== jsValue;
if (
shouldUpdateNativeSwitch &&
// $FlowFixMe[method-unbinding]
nativeSwitchRef.current?.setNativeProps != null
) {
if (Platform.OS === 'android') {
AndroidSwitchCommands.setNativeValue(nativeSwitchRef.current, jsValue);
} else {
SwitchCommands.setValue(nativeSwitchRef.current, jsValue);
}
}
}, [value, native]);
if (Platform.OS === 'android') {
const {onTintColor, tintColor, ...androidProps} = restProps;
const {accessibilityState} = androidProps;
const _disabled =
disabled != null ? disabled : accessibilityState?.disabled;
const _accessibilityState: ?AccessibilityState =
_disabled !== accessibilityState?.disabled
? {...accessibilityState, disabled: _disabled}
: accessibilityState;
const platformProps = {
accessibilityState: _accessibilityState,
enabled: _disabled !== true,
on: value === true,
style,
thumbTintColor: thumbColor,
trackColorForFalse: trackColorForFalse,
trackColorForTrue: trackColorForTrue,
trackTintColor: value === true ? trackColorForTrue : trackColorForFalse,
};
return (
<AndroidSwitchNativeComponent
{...androidProps}
{...platformProps}
accessibilityRole={props.accessibilityRole ?? 'switch'}
onChange={handleChange}
onResponderTerminationRequest={returnsFalse}
onStartShouldSetResponder={returnsTrue}
ref={ref}
/>
);
} else {
const platformProps = {
disabled,
onTintColor: trackColorForTrue,
style: StyleSheet.compose(
{alignSelf: 'flex-start' as const},
StyleSheet.compose(
style,
ios_backgroundColor == null
? null
: {
backgroundColor: ios_backgroundColor,
borderRadius: 16,
},
),
),
thumbTintColor: thumbColor,
tintColor: trackColorForFalse,
value: value === true,
};
return (
<SwitchNativeComponent
{...restProps}
{...platformProps}
accessibilityRole={props.accessibilityRole ?? 'switch'}
onChange={handleChange}
onResponderTerminationRequest={returnsFalse}
onStartShouldSetResponder={returnsTrue}
ref={ref}
/>
);
}
};
export default Switch;

View File

@@ -0,0 +1,12 @@
/**
* 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.
*
* @flow strict-local
* @format
*/
export * from '../../../src/private/specs_DEPRECATED/components/SwitchNativeComponent';
export {default} from '../../../src/private/specs_DEPRECATED/components/SwitchNativeComponent';

View File

@@ -0,0 +1,735 @@
/**
* 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.
*
* @flow strict-local
* @format
*/
import type {HostComponent} from '../../../src/private/types/HostComponent';
import type {PartialViewConfig} from '../../Renderer/shims/ReactNativeTypes';
import type {ColorValue, TextStyleProp} from '../../StyleSheet/StyleSheet';
import type {
BubblingEventHandler,
DirectEventHandler,
Double,
Float,
Int32,
WithDefault,
} from '../../Types/CodegenTypes';
import type {ViewProps} from '../View/ViewPropTypes';
import type {TextInputNativeCommands} from './TextInputNativeCommands';
import * as NativeComponentRegistry from '../../NativeComponent/NativeComponentRegistry';
import codegenNativeCommands from '../../Utilities/codegenNativeCommands';
export type KeyboardType =
// Cross Platform
| 'default'
| 'email-address'
| 'numeric'
| 'phone-pad'
| 'number-pad'
| 'decimal-pad'
| 'url'
// iOS-only
| 'ascii-capable'
| 'numbers-and-punctuation'
| 'name-phone-pad'
| 'twitter'
| 'web-search'
// Android-only
| 'visible-password';
export type ReturnKeyType =
// Cross Platform
| 'done'
| 'go'
| 'next'
| 'search'
| 'send'
// Android-only
| 'none'
| 'previous'
// iOS-only
| 'default'
| 'emergency-call'
| 'google'
| 'join'
| 'route'
| 'yahoo';
export type SubmitBehavior = 'submit' | 'blurAndSubmit' | 'newline';
export type AndroidTextInputNativeProps = $ReadOnly<{
// This allows us to inherit everything from ViewProps except for style (see below)
// This must be commented for Fabric codegen to work.
...Omit<ViewProps, 'style'>,
/**
* Android props after this
*/
/**
* Specifies autocomplete hints for the system, so it can provide autofill. On Android, the system will always attempt to offer autofill by using heuristics to identify the type of content.
* To disable autocomplete, set `autoComplete` to `off`.
*
* *Android Only*
*
* Possible values for `autoComplete` are:
*
* - `birthdate-day`
* - `birthdate-full`
* - `birthdate-month`
* - `birthdate-year`
* - `cc-csc`
* - `cc-exp`
* - `cc-exp-day`
* - `cc-exp-month`
* - `cc-exp-year`
* - `cc-number`
* - `email`
* - `gender`
* - `name`
* - `name-family`
* - `name-given`
* - `name-middle`
* - `name-middle-initial`
* - `name-prefix`
* - `name-suffix`
* - `password`
* - `password-new`
* - `postal-address`
* - `postal-address-country`
* - `postal-address-extended`
* - `postal-address-extended-postal-code`
* - `postal-address-locality`
* - `postal-address-region`
* - `postal-code`
* - `street-address`
* - `sms-otp`
* - `tel`
* - `tel-country-code`
* - `tel-national`
* - `tel-device`
* - `username`
* - `username-new`
* - `off`
*
* @platform android
*/
autoComplete?: WithDefault<
| 'birthdate-day'
| 'birthdate-full'
| 'birthdate-month'
| 'birthdate-year'
| 'cc-csc'
| 'cc-exp'
| 'cc-exp-day'
| 'cc-exp-month'
| 'cc-exp-year'
| 'cc-number'
| 'email'
| 'gender'
| 'name'
| 'name-family'
| 'name-given'
| 'name-middle'
| 'name-middle-initial'
| 'name-prefix'
| 'name-suffix'
| 'password'
| 'password-new'
| 'postal-address'
| 'postal-address-country'
| 'postal-address-extended'
| 'postal-address-extended-postal-code'
| 'postal-address-locality'
| 'postal-address-region'
| 'postal-code'
| 'street-address'
| 'sms-otp'
| 'tel'
| 'tel-country-code'
| 'tel-national'
| 'tel-device'
| 'username'
| 'username-new'
| 'off',
'off',
>,
/**
* Sets the return key to the label. Use it instead of `returnKeyType`.
* @platform android
*/
returnKeyLabel?: ?string,
/**
* Sets the number of lines for a `TextInput`. Use it with multiline set to
* `true` to be able to fill the lines.
* @platform android
*/
numberOfLines?: ?Int32,
/**
* When `false`, if there is a small amount of space available around a text input
* (e.g. landscape orientation on a phone), the OS may choose to have the user edit
* the text inside of a full screen text input mode. When `true`, this feature is
* disabled and users will always edit the text directly inside of the text input.
* Defaults to `false`.
* @platform android
*/
disableFullscreenUI?: ?boolean,
/**
* Set text break strategy on Android API Level 23+, possible values are `simple`, `highQuality`, `balanced`
* The default value is `simple`.
* @platform android
*/
textBreakStrategy?: WithDefault<
'simple' | 'highQuality' | 'balanced',
'simple',
>,
/**
* The color of the `TextInput` underline.
* @platform android
*/
underlineColorAndroid?: ?ColorValue,
/**
* If defined, the provided image resource will be rendered on the left.
* The image resource must be inside `/android/app/src/main/res/drawable` and referenced
* like
* ```
* <TextInput
* inlineImageLeft='search_icon'
* />
* ```
* @platform android
*/
inlineImageLeft?: ?string,
/**
* Padding between the inline image, if any, and the text input itself.
* @platform android
*/
inlineImagePadding?: ?Int32,
importantForAutofill?: string /*?(
| 'auto'
| 'no'
| 'noExcludeDescendants'
| 'yes'
| 'yesExcludeDescendants'
),*/,
/**
* When `false`, it will prevent the soft keyboard from showing when the field is focused.
* Defaults to `true`.
*/
showSoftInputOnFocus?: ?boolean,
/**
* TextInput props after this
*/
/**
* Can tell `TextInput` to automatically capitalize certain characters.
*
* - `characters`: all characters.
* - `words`: first letter of each word.
* - `sentences`: first letter of each sentence (*default*).
* - `none`: don't auto capitalize anything.
*/
autoCapitalize?: WithDefault<
'none' | 'sentences' | 'words' | 'characters',
'none',
>,
/**
* If `false`, disables auto-correct. The default value is `true`.
*/
autoCorrect?: ?boolean,
/**
* If `true`, focuses the input on `componentDidMount`.
* The default value is `false`.
*/
autoFocus?: ?boolean,
/**
* Specifies whether fonts should scale to respect Text Size accessibility settings. The
* default is `true`.
*/
allowFontScaling?: ?boolean,
/**
* Specifies largest possible scale a font can reach when `allowFontScaling` is enabled.
* Possible values:
* `null/undefined` (default): inherit from the parent node or the global default (0)
* `0`: no max, ignore parent/global default
* `>= 1`: sets the maxFontSizeMultiplier of this node to this value
*/
maxFontSizeMultiplier?: ?Float,
/**
* If `false`, text is not editable. The default value is `true`.
*/
editable?: ?boolean,
/**
* Determines which keyboard to open, e.g.`numeric`.
*
* The following values work across platforms:
*
* - `default`
* - `numeric`
* - `number-pad`
* - `decimal-pad`
* - `email-address`
* - `phone-pad`
* - `url`
*
* *Android Only*
*
* The following values work on Android only:
*
* - `visible-password`
*/
keyboardType?: WithDefault<KeyboardType, 'default'>,
/**
* Determines how the return key should look. On Android you can also use
* `returnKeyLabel`.
*
* *Cross platform*
*
* The following values work across platforms:
*
* - `done`
* - `go`
* - `next`
* - `search`
* - `send`
*
* *Android Only*
*
* The following values work on Android only:
*
* - `none`
* - `previous`
*/
returnKeyType?: WithDefault<ReturnKeyType, 'done'>,
/**
* Limits the maximum number of characters that can be entered. Use this
* instead of implementing the logic in JS to avoid flicker.
*/
maxLength?: ?Int32,
/**
* If `true`, the text input can be multiple lines.
* The default value is `false`.
*/
multiline?: ?boolean,
/**
* Callback that is called when the text input is blurred.
* `target` is the reactTag of the element
*/
onBlur?: ?BubblingEventHandler<$ReadOnly<{target: Int32}>>,
/**
* Callback that is called when the text input is focused.
* `target` is the reactTag of the element
*/
onFocus?: ?BubblingEventHandler<$ReadOnly<{target: Int32}>>,
/**
* Callback that is called when the text input's text changes.
* `target` is the reactTag of the element
* TODO: differentiate between onChange and onChangeText
*/
onChange?: ?BubblingEventHandler<
$ReadOnly<{target: Int32, eventCount: Int32, text: string}>,
>,
/**
* Callback that is called when the text input's text changes.
* Changed text is passed as an argument to the callback handler.
* TODO: differentiate between onChange and onChangeText
*/
onChangeText?: ?BubblingEventHandler<
$ReadOnly<{target: Int32, eventCount: Int32, text: string}>,
>,
/**
* Callback that is called when the text input's content size changes.
* This will be called with
* `{ nativeEvent: { contentSize: { width, height } } }`.
*
* Only called for multiline text inputs.
*/
onContentSizeChange?: ?DirectEventHandler<
$ReadOnly<{
target: Int32,
contentSize: $ReadOnly<{width: Double, height: Double}>,
}>,
>,
/**
* Callback that is called when text input ends.
*/
onEndEditing?: ?BubblingEventHandler<
$ReadOnly<{target: Int32, text: string}>,
>,
/**
* Callback that is called when the text input selection is changed.
* This will be called with
* `{ nativeEvent: { selection: { start, end } } }`.
*/
onSelectionChange?: ?DirectEventHandler<
$ReadOnly<{
target: Int32,
selection: $ReadOnly<{start: Double, end: Double}>,
}>,
>,
/**
* Callback that is called when the text input's submit button is pressed.
* Invalid if `multiline={true}` is specified.
*/
onSubmitEditing?: ?BubblingEventHandler<
$ReadOnly<{target: Int32, text: string}>,
>,
/**
* Callback that is called when a key is pressed.
* This will be called with `{ nativeEvent: { key: keyValue } }`
* where `keyValue` is `'Enter'` or `'Backspace'` for respective keys and
* the typed-in character otherwise including `' '` for space.
* Fires before `onChange` callbacks.
*/
onKeyPress?: ?BubblingEventHandler<$ReadOnly<{target: Int32, key: string}>>,
/**
* Invoked on content scroll with `{ nativeEvent: { contentOffset: { x, y } } }`.
* May also contain other properties from ScrollEvent but on Android contentSize
* is not provided for performance reasons.
*/
onScroll?: ?DirectEventHandler<
$ReadOnly<{
target: Int32,
responderIgnoreScroll: boolean,
contentInset: $ReadOnly<{
top: Double, // always 0 on Android
bottom: Double, // always 0 on Android
left: Double, // always 0 on Android
right: Double, // always 0 on Android
}>,
contentOffset: $ReadOnly<{
x: Double,
y: Double,
}>,
contentSize: $ReadOnly<{
width: Double, // always 0 on Android
height: Double, // always 0 on Android
}>,
layoutMeasurement: $ReadOnly<{
width: Double,
height: Double,
}>,
velocity: $ReadOnly<{
x: Double, // always 0 on Android
y: Double, // always 0 on Android
}>,
}>,
>,
/**
* The string that will be rendered before text input has been entered.
*/
placeholder?: ?Stringish,
/**
* The text color of the placeholder string.
*/
placeholderTextColor?: ?ColorValue,
/**
* If `true`, the text input obscures the text entered so that sensitive text
* like passwords stay secure. The default value is `false`. Does not work with 'multiline={true}'.
*/
secureTextEntry?: ?boolean,
/**
* The highlight and cursor color of the text input.
*/
selectionColor?: ?ColorValue,
/**
* The text selection handle color.
*/
selectionHandleColor?: ?ColorValue,
/**
* The start and end of the text input's selection. Set start and end to
* the same value to position the cursor.
*/
selection?: ?$ReadOnly<{
start: Int32,
end?: ?Int32,
}>,
/**
* The value to show for the text input. `TextInput` is a controlled
* component, which means the native value will be forced to match this
* value prop if provided. For most uses, this works great, but in some
* cases this may cause flickering - one common cause is preventing edits
* by keeping value the same. In addition to simply setting the same value,
* either set `editable={false}`, or set/update `maxLength` to prevent
* unwanted edits without flicker.
*/
value?: ?string,
/**
* Provides an initial value that will change when the user starts typing.
* Useful for simple use-cases where you do not want to deal with listening
* to events and updating the value prop to keep the controlled state in sync.
*/
defaultValue?: ?string,
/**
* If `true`, all text will automatically be selected on focus.
*/
selectTextOnFocus?: ?boolean,
/**
* If `true`, the text field will blur when submitted.
* The default value is true for single-line fields and false for
* multiline fields. Note that for multiline fields, setting `blurOnSubmit`
* to `true` means that pressing return will blur the field and trigger the
* `onSubmitEditing` event instead of inserting a newline into the field.
*
* @deprecated
* Note that `submitBehavior` now takes the place of `blurOnSubmit` and will
* override any behavior defined by `blurOnSubmit`.
* @see submitBehavior
*/
blurOnSubmit?: ?boolean,
/**
* When the return key is pressed,
*
* For single line inputs:
*
* - `'newline`' defaults to `'blurAndSubmit'`
* - `undefined` defaults to `'blurAndSubmit'`
*
* For multiline inputs:
*
* - `'newline'` adds a newline
* - `undefined` defaults to `'newline'`
*
* For both single line and multiline inputs:
*
* - `'submit'` will only send a submit event and not blur the input
* - `'blurAndSubmit`' will both blur the input and send a submit event
*/
submitBehavior?: ?SubmitBehavior,
/**
* Note that not all Text styles are supported, an incomplete list of what is not supported includes:
*
* - `borderLeftWidth`
* - `borderTopWidth`
* - `borderRightWidth`
* - `borderBottomWidth`
* - `borderTopLeftRadius`
* - `borderTopRightRadius`
* - `borderBottomRightRadius`
* - `borderBottomLeftRadius`
*
* see [Issue#7070](https://github.com/facebook/react-native/issues/7070)
* for more detail.
*
* [Styles](docs/style.html)
*/
// TODO: figure out what to do with this style prop for codegen/Fabric purposes
// This must be commented for Fabric codegen to work; it's currently not possible
// to override the default View style prop in codegen.
style?: ?TextStyleProp,
/**
* If `true`, caret is hidden. The default value is `false`.
* This property is supported only for single-line TextInput component on iOS.
*/
caretHidden?: ?boolean,
/*
* If `true`, contextMenuHidden is hidden. The default value is `false`.
*/
contextMenuHidden?: ?boolean,
/**
* The following are props that `BaseTextShadowNode` takes. It is unclear if they
* are used by TextInput.
*/
textShadowColor?: ?ColorValue,
textShadowRadius?: ?Float,
textDecorationLine?: ?string,
fontStyle?: ?string,
textShadowOffset?: ?$ReadOnly<{width?: ?Double, height?: ?Double}>,
lineHeight?: ?Float,
textTransform?: ?string,
color?: ?Int32,
letterSpacing?: ?Float,
fontSize?: ?Float,
textAlign?: ?string,
includeFontPadding?: ?boolean,
fontWeight?: ?string,
fontFamily?: ?string,
/**
* I cannot find where these are defined but JS complains without them.
*/
textAlignVertical?: ?string,
cursorColor?: ?ColorValue,
/**
* "Private" fields used by TextInput.js and not users of this component directly
*/
mostRecentEventCount: Int32,
text?: ?string,
}>;
type NativeType = HostComponent<AndroidTextInputNativeProps>;
type NativeCommands = TextInputNativeCommands<NativeType>;
export const Commands: NativeCommands = codegenNativeCommands<NativeCommands>({
supportedCommands: ['focus', 'blur', 'setTextAndSelection'],
});
export const __INTERNAL_VIEW_CONFIG: PartialViewConfig = {
uiViewClassName: 'AndroidTextInput',
bubblingEventTypes: {
topEndEditing: {
phasedRegistrationNames: {
bubbled: 'onEndEditing',
captured: 'onEndEditingCapture',
},
},
topKeyPress: {
phasedRegistrationNames: {
bubbled: 'onKeyPress',
captured: 'onKeyPressCapture',
},
},
topSubmitEditing: {
phasedRegistrationNames: {
bubbled: 'onSubmitEditing',
captured: 'onSubmitEditingCapture',
},
},
},
directEventTypes: {
topScroll: {
registrationName: 'onScroll',
},
},
validAttributes: {
acceptDragAndDropTypes: true,
maxFontSizeMultiplier: true,
adjustsFontSizeToFit: true,
minimumFontScale: true,
autoFocus: true,
placeholder: true,
inlineImagePadding: true,
contextMenuHidden: true,
textShadowColor: {
process: require('../../StyleSheet/processColor').default,
},
maxLength: true,
selectTextOnFocus: true,
textShadowRadius: true,
underlineColorAndroid: {
process: require('../../StyleSheet/processColor').default,
},
textDecorationLine: true,
submitBehavior: true,
textAlignVertical: true,
fontStyle: true,
textShadowOffset: true,
selectionColor: {process: require('../../StyleSheet/processColor').default},
selectionHandleColor: {
process: require('../../StyleSheet/processColor').default,
},
placeholderTextColor: {
process: require('../../StyleSheet/processColor').default,
},
importantForAutofill: true,
lineHeight: true,
textTransform: true,
returnKeyType: true,
keyboardType: true,
multiline: true,
color: {process: require('../../StyleSheet/processColor').default},
autoComplete: true,
numberOfLines: true,
letterSpacing: true,
returnKeyLabel: true,
fontSize: true,
onKeyPress: true,
cursorColor: {process: require('../../StyleSheet/processColor').default},
text: true,
showSoftInputOnFocus: true,
textAlign: true,
autoCapitalize: true,
autoCorrect: true,
caretHidden: true,
secureTextEntry: true,
textBreakStrategy: true,
onScroll: true,
onContentSizeChange: true,
disableFullscreenUI: true,
includeFontPadding: true,
fontWeight: true,
fontFamily: true,
allowFontScaling: true,
onSelectionChange: true,
mostRecentEventCount: true,
inlineImageLeft: true,
editable: true,
fontVariant: true,
borderBottomRightRadius: true,
borderBottomColor: {
process: require('../../StyleSheet/processColor').default,
},
borderRadius: true,
borderRightColor: {
process: require('../../StyleSheet/processColor').default,
},
borderColor: {process: require('../../StyleSheet/processColor').default},
borderTopRightRadius: true,
borderStyle: true,
borderBottomLeftRadius: true,
borderLeftColor: {
process: require('../../StyleSheet/processColor').default,
},
borderTopLeftRadius: true,
borderTopColor: {process: require('../../StyleSheet/processColor').default},
},
};
let AndroidTextInputNativeComponent =
NativeComponentRegistry.get<AndroidTextInputNativeProps>(
'AndroidTextInput',
() => __INTERNAL_VIEW_CONFIG,
);
// flowlint-next-line unclear-type:off
export default ((AndroidTextInputNativeComponent: any): HostComponent<AndroidTextInputNativeProps>);

View File

@@ -0,0 +1,34 @@
/**
* 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
*/
import type * as React from 'react';
import {ColorValue, StyleProp} from '../../StyleSheet/StyleSheet';
import {ViewStyle} from '../../StyleSheet/StyleSheetTypes';
/**
* A component which enables customization of the keyboard input accessory view on iOS. The input accessory view is
* displayed above the keyboard whenever a TextInput has focus. This component can be used to create custom toolbars.
*
* To use this component wrap your custom toolbar with the InputAccessoryView component, and set a nativeID. Then, pass
* that nativeID as the inputAccessoryViewID of whatever TextInput you desire.
*/
export class InputAccessoryView extends React.Component<InputAccessoryViewProps> {}
export interface InputAccessoryViewProps {
backgroundColor?: ColorValue | undefined;
children?: React.ReactNode | undefined;
/**
* An ID which is used to associate this InputAccessoryView to specified TextInput(s).
*/
nativeID?: string | undefined;
style?: StyleProp<ViewStyle> | undefined;
}

View File

@@ -0,0 +1,125 @@
/**
* 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.
*
* @flow strict-local
* @format
*/
import SafeAreaView from '../../Components/SafeAreaView/SafeAreaView';
import StyleSheet, {
type ColorValue,
type ViewStyleProp,
} from '../../StyleSheet/StyleSheet';
import Platform from '../../Utilities/Platform';
import useWindowDimensions from '../../Utilities/useWindowDimensions';
import RCTInputAccessoryViewNativeComponent from './RCTInputAccessoryViewNativeComponent';
import * as React from 'react';
/**
* Note: iOS only
*
* A component which enables customization of the keyboard input accessory view.
* The input accessory view is displayed above the keyboard whenever a TextInput
* has focus. This component can be used to create custom toolbars.
*
* To use this component wrap your custom toolbar with the
* InputAccessoryView component, and set a nativeID. Then, pass that nativeID
* as the inputAccessoryViewID of whatever TextInput you desire. A simple
* example:
*
* ```ReactNativeWebPlayer
* import React, { Component } from 'react';
* import { AppRegistry, TextInput, InputAccessoryView, Button } from 'react-native';
*
* export default class UselessTextInput extends Component {
* constructor(props) {
* super(props);
* this.state = {text: 'Placeholder Text'};
* }
*
* render() {
* const inputAccessoryViewID = "uniqueID";
* return (
* <View>
* <ScrollView keyboardDismissMode="interactive">
* <TextInput
* style={{
* padding: 10,
* paddingTop: 50,
* }}
* inputAccessoryViewID=inputAccessoryViewID
* onChangeText={text => this.setState({text})}
* value={this.state.text}
* />
* </ScrollView>
* <InputAccessoryView nativeID=inputAccessoryViewID>
* <Button
* onPress={() => this.setState({text: 'Placeholder Text'})}
* title="Reset Text"
* />
* </InputAccessoryView>
* </View>
* );
* }
* }
*
* // skip this line if using Create React Native App
* AppRegistry.registerComponent('AwesomeProject', () => UselessTextInput);
* ```
*
* This component can also be used to create sticky text inputs (text inputs
* which are anchored to the top of the keyboard). To do this, wrap a
* TextInput with the InputAccessoryView component, and don't set a nativeID.
* For an example, look at InputAccessoryViewExample.js in RNTester.
*/
export type InputAccessoryViewProps = $ReadOnly<{
+children: React.Node,
/**
* An ID which is used to associate this `InputAccessoryView` to
* specified TextInput(s).
*/
nativeID?: ?string,
style?: ?ViewStyleProp,
backgroundColor?: ?ColorValue,
}>;
const InputAccessoryView: React.ComponentType<InputAccessoryViewProps> = (
props: InputAccessoryViewProps,
) => {
const {width} = useWindowDimensions();
if (Platform.OS === 'ios') {
if (React.Children.count(props.children) === 0) {
return null;
}
return (
<RCTInputAccessoryViewNativeComponent
style={[props.style, styles.container]}
nativeID={props.nativeID}
backgroundColor={props.backgroundColor}>
<SafeAreaView style={[styles.safeAreaView, {width}]}>
{props.children}
</SafeAreaView>
</RCTInputAccessoryViewNativeComponent>
);
} else {
console.warn('<InputAccessoryView> is only supported on iOS.');
return null;
}
};
const styles = StyleSheet.create({
container: {
position: 'absolute',
},
safeAreaView: {
flex: 1,
},
});
export default InputAccessoryView;

View File

@@ -0,0 +1,12 @@
/**
* 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.
*
* @flow strict-local
* @format
*/
export * from '../../../src/private/specs_DEPRECATED/components/RCTInputAccessoryViewNativeComponent';
export {default} from '../../../src/private/specs_DEPRECATED/components/RCTInputAccessoryViewNativeComponent';

View File

@@ -0,0 +1,43 @@
/**
* 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.
*
* @flow strict-local
* @format
*/
import type {HostComponent} from '../../../src/private/types/HostComponent';
import type {PartialViewConfig} from '../../Renderer/shims/ReactNativeTypes';
import type {TextInputNativeCommands} from './TextInputNativeCommands';
import * as NativeComponentRegistry from '../../NativeComponent/NativeComponentRegistry';
import codegenNativeCommands from '../../Utilities/codegenNativeCommands';
import RCTTextInputViewConfig from './RCTTextInputViewConfig';
type NativeType = HostComponent<{...}>;
type NativeCommands = TextInputNativeCommands<NativeType>;
export const Commands: NativeCommands = codegenNativeCommands<NativeCommands>({
supportedCommands: ['focus', 'blur', 'setTextAndSelection'],
});
export const __INTERNAL_VIEW_CONFIG: PartialViewConfig = {
uiViewClassName: 'RCTMultilineTextInputView',
...RCTTextInputViewConfig,
validAttributes: {
...RCTTextInputViewConfig.validAttributes,
dataDetectorTypes: true,
},
};
const MultilineTextInputNativeComponent: HostComponent<{...}> =
NativeComponentRegistry.get<{...}>(
'RCTMultilineTextInputView',
() => __INTERNAL_VIEW_CONFIG,
);
// flowlint-next-line unclear-type:off
export default ((MultilineTextInputNativeComponent: any): HostComponent<{...}>);

View File

@@ -0,0 +1,41 @@
/**
* 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.
*
* @flow strict-local
* @format
*/
import type {HostComponent} from '../../../src/private/types/HostComponent';
import type {PartialViewConfig} from '../../Renderer/shims/ReactNativeTypes';
import type {TextInputNativeCommands} from './TextInputNativeCommands';
import * as NativeComponentRegistry from '../../NativeComponent/NativeComponentRegistry';
import codegenNativeCommands from '../../Utilities/codegenNativeCommands';
import RCTTextInputViewConfig from './RCTTextInputViewConfig';
type NativeType = HostComponent<{...}>;
type NativeCommands = TextInputNativeCommands<NativeType>;
export const Commands: NativeCommands = codegenNativeCommands<NativeCommands>({
supportedCommands: ['focus', 'blur', 'setTextAndSelection'],
});
export const __INTERNAL_VIEW_CONFIG: PartialViewConfig = {
uiViewClassName: 'RCTSinglelineTextInputView',
...RCTTextInputViewConfig,
};
const SinglelineTextInputNativeComponent: HostComponent<{...}> =
NativeComponentRegistry.get<{...}>(
'RCTSinglelineTextInputView',
() => __INTERNAL_VIEW_CONFIG,
);
// flowlint-next-line unclear-type:off
export default ((SinglelineTextInputNativeComponent: any): HostComponent<{
...
}>);

View File

@@ -0,0 +1,170 @@
/**
* 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.
*
* @flow strict-local
* @format
*/
import type {PartialViewConfig} from '../../Renderer/shims/ReactNativeTypes';
import {ConditionallyIgnoredEventHandlers} from '../../NativeComponent/ViewConfigIgnore';
type PartialViewConfigWithoutName = Omit<PartialViewConfig, 'uiViewClassName'>;
const RCTTextInputViewConfig: PartialViewConfigWithoutName = {
bubblingEventTypes: {
topBlur: {
phasedRegistrationNames: {
bubbled: 'onBlur',
captured: 'onBlurCapture',
},
},
topChange: {
phasedRegistrationNames: {
bubbled: 'onChange',
captured: 'onChangeCapture',
},
},
topEndEditing: {
phasedRegistrationNames: {
bubbled: 'onEndEditing',
captured: 'onEndEditingCapture',
},
},
topFocus: {
phasedRegistrationNames: {
bubbled: 'onFocus',
captured: 'onFocusCapture',
},
},
topKeyPress: {
phasedRegistrationNames: {
bubbled: 'onKeyPress',
captured: 'onKeyPressCapture',
},
},
topSubmitEditing: {
phasedRegistrationNames: {
bubbled: 'onSubmitEditing',
captured: 'onSubmitEditingCapture',
},
},
topTouchCancel: {
phasedRegistrationNames: {
bubbled: 'onTouchCancel',
captured: 'onTouchCancelCapture',
},
},
topTouchEnd: {
phasedRegistrationNames: {
bubbled: 'onTouchEnd',
captured: 'onTouchEndCapture',
},
},
topTouchMove: {
phasedRegistrationNames: {
bubbled: 'onTouchMove',
captured: 'onTouchMoveCapture',
},
},
},
directEventTypes: {
topScroll: {
registrationName: 'onScroll',
},
topSelectionChange: {
registrationName: 'onSelectionChange',
},
topContentSizeChange: {
registrationName: 'onContentSizeChange',
},
topChangeSync: {
registrationName: 'onChangeSync',
},
topKeyPressSync: {
registrationName: 'onKeyPressSync',
},
},
validAttributes: {
acceptDragAndDropTypes: true,
dynamicTypeRamp: true,
fontSize: true,
fontWeight: true,
fontVariant: true,
// flowlint-next-line untyped-import:off
textShadowOffset: {
diff: require('../../Utilities/differ/sizesDiffer').default,
},
allowFontScaling: true,
fontStyle: true,
textTransform: true,
textAlign: true,
fontFamily: true,
lineHeight: true,
isHighlighted: true,
writingDirection: true,
textDecorationLine: true,
textShadowRadius: true,
letterSpacing: true,
textDecorationStyle: true,
textDecorationColor: {
process: require('../../StyleSheet/processColor').default,
},
color: {process: require('../../StyleSheet/processColor').default},
maxFontSizeMultiplier: true,
textShadowColor: {
process: require('../../StyleSheet/processColor').default,
},
editable: true,
inputAccessoryViewID: true,
inputAccessoryViewButtonLabel: true,
caretHidden: true,
enablesReturnKeyAutomatically: true,
placeholderTextColor: {
process: require('../../StyleSheet/processColor').default,
},
clearButtonMode: true,
keyboardType: true,
selection: true,
returnKeyType: true,
submitBehavior: true,
mostRecentEventCount: true,
scrollEnabled: true,
selectionColor: {process: require('../../StyleSheet/processColor').default},
contextMenuHidden: true,
secureTextEntry: true,
placeholder: true,
autoCorrect: true,
multiline: true,
numberOfLines: true,
textContentType: true,
maxLength: true,
autoCapitalize: true,
keyboardAppearance: true,
passwordRules: true,
spellCheck: true,
selectTextOnFocus: true,
text: true,
clearTextOnFocus: true,
showSoftInputOnFocus: true,
autoFocus: true,
lineBreakStrategyIOS: true,
lineBreakModeIOS: true,
smartInsertDelete: true,
...ConditionallyIgnoredEventHandlers({
onChange: true,
onSelectionChange: true,
onContentSizeChange: true,
onScroll: true,
onChangeSync: true,
onKeyPressSync: true,
}),
disableKeyboardShortcuts: true,
},
};
export default RCTTextInputViewConfig as PartialViewConfigWithoutName;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,990 @@
/**
* 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.
*
* @flow strict-local
* @format
*/
import type {HostInstance} from '../../../src/private/types/HostInstance';
import type {____TextStyle_Internal as TextStyleInternal} from '../../StyleSheet/StyleSheetTypes';
import type {
BlurEvent,
FocusEvent,
GestureResponderEvent,
ScrollEvent,
} from '../../Types/CoreEventTypes';
import type {
AutoCapitalize,
EnterKeyHintType,
EnterKeyHintTypeAndroid,
EnterKeyHintTypeIOS,
EnterKeyHintTypeOptions,
InputModeOptions,
KeyboardType,
KeyboardTypeAndroid,
KeyboardTypeIOS,
KeyboardTypeOptions,
ReturnKeyType,
ReturnKeyTypeAndroid,
ReturnKeyTypeIOS,
ReturnKeyTypeOptions,
Selection,
SubmitBehavior,
TextContentType,
TextInputAndroidProps,
TextInputBlurEvent,
TextInputChangeEvent,
TextInputContentSizeChangeEvent,
TextInputEditingEvent,
TextInputEndEditingEvent,
TextInputEvent,
TextInputFocusEvent,
TextInputInstance,
TextInputIOSProps,
TextInputKeyPressEvent,
TextInputProps,
TextInputSelectionChangeEvent,
TextInputSubmitEditingEvent,
TextInputType,
} from './TextInput.flow';
import usePressability from '../../Pressability/usePressability';
import flattenStyle from '../../StyleSheet/flattenStyle';
import StyleSheet, {type TextStyleProp} from '../../StyleSheet/StyleSheet';
import Text from '../../Text/Text';
import TextAncestorContext from '../../Text/TextAncestorContext';
import Platform from '../../Utilities/Platform';
import useMergeRefs from '../../Utilities/useMergeRefs';
import TextInputState from './TextInputState';
import invariant from 'invariant';
import nullthrows from 'nullthrows';
import * as React from 'react';
import {useCallback, useLayoutEffect, useMemo, useRef, useState} from 'react';
let AndroidTextInput;
let AndroidTextInputCommands;
let RCTSinglelineTextInputView;
let RCTSinglelineTextInputNativeCommands;
let RCTMultilineTextInputView;
let RCTMultilineTextInputNativeCommands;
if (Platform.OS === 'android') {
AndroidTextInput = require('./AndroidTextInputNativeComponent').default;
AndroidTextInputCommands =
require('./AndroidTextInputNativeComponent').Commands;
} else if (Platform.OS === 'ios') {
RCTSinglelineTextInputView =
require('./RCTSingelineTextInputNativeComponent').default;
RCTSinglelineTextInputNativeCommands =
require('./RCTSingelineTextInputNativeComponent').Commands;
RCTMultilineTextInputView =
require('./RCTMultilineTextInputNativeComponent').default;
RCTMultilineTextInputNativeCommands =
require('./RCTMultilineTextInputNativeComponent').Commands;
}
export type {
AutoCapitalize,
BlurEvent,
EnterKeyHintType,
EnterKeyHintTypeAndroid,
EnterKeyHintTypeIOS,
EnterKeyHintTypeOptions,
FocusEvent,
InputModeOptions,
KeyboardType,
KeyboardTypeAndroid,
KeyboardTypeIOS,
KeyboardTypeOptions,
ReturnKeyType,
ReturnKeyTypeAndroid,
ReturnKeyTypeIOS,
ReturnKeyTypeOptions,
SubmitBehavior,
TextContentType,
TextInputAndroidProps,
TextInputBlurEvent,
TextInputChangeEvent,
TextInputContentSizeChangeEvent,
TextInputEditingEvent,
TextInputEndEditingEvent,
TextInputEvent,
TextInputFocusEvent,
TextInputIOSProps,
TextInputKeyPressEvent,
TextInputProps,
TextInputSelectionChangeEvent,
TextInputSubmitEditingEvent,
};
type TextInputStateType = $ReadOnly<{
/**
* @deprecated Use currentlyFocusedInput
* Returns the ID of the currently focused text field, if one exists
* If no text field is focused it returns null
*/
currentlyFocusedField: () => ?number,
/**
* Returns the ref of the currently focused text field, if one exists
* If no text field is focused it returns null
*/
currentlyFocusedInput: () => ?HostInstance,
/**
* @param textField ref of the text field to focus
* Focuses the specified text field
* noop if the text field was already focused
*/
focusTextInput: (textField: ?HostInstance) => void,
/**
* @param textField ref of the text field to focus
* Unfocuses the specified text field
* noop if it wasn't focused
*/
blurTextInput: (textField: ?HostInstance) => void,
}>;
type ViewCommands = $NonMaybeType<
| typeof AndroidTextInputCommands
| typeof RCTMultilineTextInputNativeCommands
| typeof RCTSinglelineTextInputNativeCommands,
>;
type LastNativeSelection = {
selection: Selection,
mostRecentEventCount: number,
};
const emptyFunctionThatReturnsTrue = () => true;
/**
* This hook handles the synchronization between the state of the text input
* in native and in JavaScript. This is necessary due to the asynchronous nature
* of text input events.
*/
function useTextInputStateSynchronization({
props,
mostRecentEventCount,
selection,
inputRef,
text,
viewCommands,
}: {
props: TextInputProps,
mostRecentEventCount: number,
selection: ?Selection,
inputRef: React.RefObject<null | TextInputInstance>,
text?: string,
viewCommands: ViewCommands,
}): {
setLastNativeText: string => void,
setLastNativeSelection: LastNativeSelection => void,
} {
const [lastNativeText, setLastNativeText] = useState<?Stringish>(props.value);
const [lastNativeSelectionState, setLastNativeSelection] =
useState<LastNativeSelection>({
selection: {start: -1, end: -1},
mostRecentEventCount: mostRecentEventCount,
});
const lastNativeSelection = lastNativeSelectionState.selection;
// This is necessary in case native updates the text and JS decides
// that the update should be ignored and we should stick with the value
// that we have in JS.
useLayoutEffect(() => {
const nativeUpdate: {text?: string, selection?: Selection} = {};
if (lastNativeText !== props.value && typeof props.value === 'string') {
nativeUpdate.text = props.value;
setLastNativeText(props.value);
}
if (
selection &&
lastNativeSelection &&
(lastNativeSelection.start !== selection.start ||
lastNativeSelection.end !== selection.end)
) {
nativeUpdate.selection = selection;
setLastNativeSelection({selection, mostRecentEventCount});
}
if (Object.keys(nativeUpdate).length === 0) {
return;
}
if (inputRef.current != null) {
viewCommands.setTextAndSelection(
inputRef.current,
mostRecentEventCount,
text,
selection?.start ?? -1,
selection?.end ?? -1,
);
}
}, [
mostRecentEventCount,
inputRef,
props.value,
props.defaultValue,
lastNativeText,
selection,
lastNativeSelection,
text,
viewCommands,
]);
return {setLastNativeText, setLastNativeSelection};
}
/**
* A foundational component for inputting text into the app via a
* keyboard. Props provide configurability for several features, such as
* auto-correction, auto-capitalization, placeholder text, and different keyboard
* types, such as a numeric keypad.
*
* The simplest use case is to plop down a `TextInput` and subscribe to the
* `onChangeText` events to read the user input. There are also other events,
* such as `onSubmitEditing` and `onFocus` that can be subscribed to. A simple
* example:
*
* ```ReactNativeWebPlayer
* import React, { Component } from 'react';
* import { AppRegistry, TextInput } from 'react-native';
*
* export default class UselessTextInput extends Component {
* constructor(props) {
* super(props);
* this.state = { text: 'Useless Placeholder' };
* }
*
* render() {
* return (
* <TextInput
* style={{height: 40, borderColor: 'gray', borderWidth: 1}}
* onChangeText={(text) => this.setState({text})}
* value={this.state.text}
* />
* );
* }
* }
*
* // skip this line if using Create React Native App
* AppRegistry.registerComponent('AwesomeProject', () => UselessTextInput);
* ```
*
* Two methods exposed via the native element are .focus() and .blur() that
* will focus or blur the TextInput programmatically.
*
* Note that some props are only available with `multiline={true/false}`.
* Additionally, border styles that apply to only one side of the element
* (e.g., `borderBottomColor`, `borderLeftWidth`, etc.) will not be applied if
* `multiline=false`. To achieve the same effect, you can wrap your `TextInput`
* in a `View`:
*
* ```ReactNativeWebPlayer
* import React, { Component } from 'react';
* import { AppRegistry, View, TextInput } from 'react-native';
*
* class UselessTextInput extends Component {
* render() {
* return (
* <TextInput
* {...this.props} // Inherit any props passed to it; e.g., multiline, numberOfLines below
* editable={true}
* maxLength={40}
* />
* );
* }
* }
*
* export default class UselessTextInputMultiline extends Component {
* constructor(props) {
* super(props);
* this.state = {
* text: 'Useless Multiline Placeholder',
* };
* }
*
* // If you type something in the text box that is a color, the background will change to that
* // color.
* render() {
* return (
* <View style={{
* backgroundColor: this.state.text,
* borderBottomColor: '#000000',
* borderBottomWidth: 1 }}
* >
* <UselessTextInput
* multiline={true}
* numberOfLines={4}
* onChangeText={(text) => this.setState({text})}
* value={this.state.text}
* />
* </View>
* );
* }
* }
*
* // skip these lines if using Create React Native App
* AppRegistry.registerComponent(
* 'AwesomeProject',
* () => UselessTextInputMultiline
* );
* ```
*
* `TextInput` has by default a border at the bottom of its view. This border
* has its padding set by the background image provided by the system, and it
* cannot be changed. Solutions to avoid this is to either not set height
* explicitly, case in which the system will take care of displaying the border
* in the correct position, or to not display the border by setting
* `underlineColorAndroid` to transparent.
*
* Note that on Android performing text selection in input can change
* app's activity `windowSoftInputMode` param to `adjustResize`.
* This may cause issues with components that have position: 'absolute'
* while keyboard is active. To avoid this behavior either specify `windowSoftInputMode`
* in AndroidManifest.xml ( https://developer.android.com/guide/topics/manifest/activity-element.html )
* or control this param programmatically with native code.
*
*/
function InternalTextInput(props: TextInputProps): React.Node {
const {
'aria-busy': ariaBusy,
'aria-checked': ariaChecked,
'aria-disabled': ariaDisabled,
'aria-expanded': ariaExpanded,
'aria-selected': ariaSelected,
accessibilityState,
id,
tabIndex,
selection: propsSelection,
selectionColor,
selectionHandleColor,
cursorColor,
...otherProps
} = props;
const inputRef = useRef<null | TextInputInstance>(null);
const selection: ?Selection =
propsSelection == null
? null
: {
start: propsSelection.start,
end: propsSelection.end ?? propsSelection.start,
};
const text =
typeof props.value === 'string'
? props.value
: typeof props.defaultValue === 'string'
? props.defaultValue
: undefined;
const viewCommands =
AndroidTextInputCommands ||
(props.multiline === true
? RCTMultilineTextInputNativeCommands
: RCTSinglelineTextInputNativeCommands);
const [mostRecentEventCount, setMostRecentEventCount] = useState<number>(0);
const {setLastNativeText, setLastNativeSelection} =
useTextInputStateSynchronization({
props,
inputRef,
mostRecentEventCount,
selection,
text,
viewCommands,
});
useLayoutEffect(() => {
const inputRefValue = inputRef.current;
if (inputRefValue != null) {
TextInputState.registerInput(inputRefValue);
return () => {
TextInputState.unregisterInput(inputRefValue);
if (TextInputState.currentlyFocusedInput() === inputRefValue) {
nullthrows(inputRefValue).blur();
}
};
}
}, []);
const setLocalRef = useCallback(
(instance: HostInstance | null) => {
// $FlowExpectedError[incompatible-type]
inputRef.current = instance;
/*
Hi reader from the future. I'm sorry for this.
This is a hack. Ideally we would forwardRef to the underlying
host component. However, since TextInput has it's own methods that can be
called as well, if we used the standard forwardRef then these
methods wouldn't be accessible and thus be a breaking change.
We have a couple of options of how to handle this:
- Return a new ref with everything we methods from both. This is problematic
because we need React to also know it is a host component which requires
internals of the class implementation of the ref.
- Break the API and have some other way to call one set of the methods or
the other. This is our long term approach as we want to eventually
get the methods on host components off the ref. So instead of calling
ref.measure() you might call ReactNative.measure(ref). This would hopefully
let the ref for TextInput then have the methods like `.clear`. Or we do it
the other way and make it TextInput.clear(textInputRef) which would be fine
too. Either way though is a breaking change that is longer term.
- Mutate this ref. :( Gross, but accomplishes what we need in the meantime
before we can get to the long term breaking change.
*/
if (instance != null) {
// Register the input immediately when the ref is set so that focus()
// can be called from ref callbacks
// Double registering during useLayoutEffect is fine, because the underlying
// state is a Set.
TextInputState.registerInput(instance);
// $FlowFixMe[prop-missing] - See the explanation above.
// $FlowFixMe[unsafe-object-assign]
Object.assign(instance, {
clear(): void {
if (inputRef.current != null) {
viewCommands.setTextAndSelection(
inputRef.current,
mostRecentEventCount,
'',
0,
0,
);
}
},
// TODO: Fix this returning true on null === null, when no input is focused
isFocused(): boolean {
return TextInputState.currentlyFocusedInput() === inputRef.current;
},
getNativeRef(): ?TextInputInstance {
return inputRef.current;
},
setSelection(start: number, end: number): void {
if (inputRef.current != null) {
viewCommands.setTextAndSelection(
inputRef.current,
mostRecentEventCount,
null,
start,
end,
);
}
},
});
}
},
[mostRecentEventCount, viewCommands],
);
// $FlowExpectedError[incompatible-type]
const ref = useMergeRefs<HostInstance>(setLocalRef, props.forwardedRef);
const _onChange = (event: TextInputChangeEvent) => {
const currentText = event.nativeEvent.text;
props.onChange && props.onChange(event);
props.onChangeText && props.onChangeText(currentText);
if (inputRef.current == null) {
// calling `props.onChange` or `props.onChangeText`
// may clean up the input itself. Exits here.
return;
}
setLastNativeText(currentText);
// This must happen last, after we call setLastNativeText.
// Different ordering can cause bugs when editing AndroidTextInputs
// with multiple Fragments.
// We must update this so that controlled input updates work.
setMostRecentEventCount(event.nativeEvent.eventCount);
};
const _onSelectionChange = (event: TextInputSelectionChangeEvent) => {
props.onSelectionChange && props.onSelectionChange(event);
if (inputRef.current == null) {
// calling `props.onSelectionChange`
// may clean up the input itself. Exits here.
return;
}
setLastNativeSelection({
selection: event.nativeEvent.selection,
mostRecentEventCount,
});
};
const _onFocus = (event: FocusEvent) => {
TextInputState.focusInput(inputRef.current);
if (props.onFocus) {
props.onFocus(event);
}
};
const _onBlur = (event: BlurEvent) => {
TextInputState.blurInput(inputRef.current);
if (props.onBlur) {
props.onBlur(event);
}
};
const _onScroll = (event: ScrollEvent) => {
props.onScroll && props.onScroll(event);
};
let textInput = null;
const multiline = props.multiline ?? false;
let submitBehavior: SubmitBehavior;
if (props.submitBehavior != null) {
// `submitBehavior` is set explicitly
if (!multiline && props.submitBehavior === 'newline') {
// For single line text inputs, `'newline'` is not a valid option
submitBehavior = 'blurAndSubmit';
} else {
submitBehavior = props.submitBehavior;
}
} else if (multiline) {
if (props.blurOnSubmit === true) {
submitBehavior = 'blurAndSubmit';
} else {
submitBehavior = 'newline';
}
} else {
// Single line
if (props.blurOnSubmit !== false) {
submitBehavior = 'blurAndSubmit';
} else {
submitBehavior = 'submit';
}
}
const accessible = props.accessible !== false;
const focusable = props.focusable !== false;
const {
editable,
hitSlop,
onPress,
onPressIn,
onPressOut,
rejectResponderTermination,
} = props;
const config = useMemo(
() => ({
hitSlop,
onPress: (event: GestureResponderEvent) => {
onPress?.(event);
if (editable !== false) {
if (inputRef.current != null) {
inputRef.current.focus();
}
}
},
onPressIn: onPressIn,
onPressOut: onPressOut,
cancelable: Platform.OS === 'ios' ? !rejectResponderTermination : null,
}),
[
editable,
hitSlop,
onPress,
onPressIn,
onPressOut,
rejectResponderTermination,
],
);
// Hide caret during test runs due to a flashing caret
// makes screenshot tests flakey
let caretHidden = props.caretHidden;
if (Platform.isTesting) {
caretHidden = true;
}
// TextInput handles onBlur and onFocus events
// so omitting onBlur and onFocus pressability handlers here.
const {onBlur, onFocus, ...eventHandlers} = usePressability(config);
const _accessibilityLabel =
props?.['aria-label'] ?? props?.accessibilityLabel;
let _accessibilityState;
if (
accessibilityState != null ||
ariaBusy != null ||
ariaChecked != null ||
ariaDisabled != null ||
ariaExpanded != null ||
ariaSelected != null
) {
_accessibilityState = {
busy: ariaBusy ?? accessibilityState?.busy,
checked: ariaChecked ?? accessibilityState?.checked,
disabled: ariaDisabled ?? accessibilityState?.disabled,
expanded: ariaExpanded ?? accessibilityState?.expanded,
selected: ariaSelected ?? accessibilityState?.selected,
};
}
// Keep the original (potentially nested) style when possible, as React can diff these more efficiently
let _style = props.style;
const flattenedStyle = flattenStyle<TextStyleProp>(props.style);
if (flattenedStyle != null) {
let overrides: ?{...TextStyleInternal} = null;
if (typeof flattenedStyle?.fontWeight === 'number') {
overrides = overrides || ({}: {...TextStyleInternal});
overrides.fontWeight =
// $FlowFixMe[incompatible-type]
(flattenedStyle.fontWeight.toString(): TextStyleInternal['fontWeight']);
}
if (flattenedStyle.verticalAlign != null) {
overrides = overrides || ({}: {...TextStyleInternal});
overrides.textAlignVertical =
verticalAlignToTextAlignVerticalMap[flattenedStyle.verticalAlign];
overrides.verticalAlign = undefined;
}
if (overrides != null) {
// $FlowFixMe[incompatible-type]
_style = [_style, overrides];
}
}
if (Platform.OS === 'ios') {
const RCTTextInputView =
props.multiline === true
? RCTMultilineTextInputView
: RCTSinglelineTextInputView;
const useMultilineDefaultStyle =
props.multiline === true &&
(flattenedStyle == null ||
(flattenedStyle.padding == null &&
flattenedStyle.paddingVertical == null &&
flattenedStyle.paddingTop == null));
const _accessibilityElementsHidden =
props['aria-hidden'] ?? props.accessibilityElementsHidden;
textInput = (
<RCTTextInputView
// Figure out imperative + forward refs.
ref={(ref: $FlowFixMe)}
{...otherProps}
{...eventHandlers}
acceptDragAndDropTypes={props.experimental_acceptDragAndDropTypes}
accessibilityLabel={_accessibilityLabel}
accessibilityState={_accessibilityState}
accessibilityElementsHidden={_accessibilityElementsHidden}
accessible={accessible}
submitBehavior={submitBehavior}
caretHidden={caretHidden}
dataDetectorTypes={props.dataDetectorTypes}
focusable={tabIndex !== undefined ? !tabIndex : focusable}
mostRecentEventCount={mostRecentEventCount}
nativeID={id ?? props.nativeID}
numberOfLines={props.rows ?? props.numberOfLines}
onBlur={_onBlur}
onChange={_onChange}
onContentSizeChange={props.onContentSizeChange}
onFocus={_onFocus}
onScroll={_onScroll}
onSelectionChange={_onSelectionChange}
onSelectionChangeShouldSetResponder={emptyFunctionThatReturnsTrue}
selection={selection}
selectionColor={selectionColor}
style={StyleSheet.compose(
useMultilineDefaultStyle ? styles.multilineDefault : null,
_style,
)}
text={text}
/>
);
} else if (Platform.OS === 'android') {
const autoCapitalize = props.autoCapitalize || 'sentences';
const _accessibilityLabelledBy =
props?.['aria-labelledby'] ?? props?.accessibilityLabelledBy;
const _importantForAccessibility =
props['aria-hidden'] === true
? ('no-hide-descendants' as const)
: undefined;
const placeholder = props.placeholder ?? '';
let children = props.children;
const childCount = React.Children.count(children);
invariant(
!(props.value != null && childCount),
'Cannot specify both value and children.',
);
if (childCount > 1) {
children = <Text>{children}</Text>;
}
// For consistency with iOS set cursor/selectionHandle color as selectionColor
const colorProps = {
selectionColor,
selectionHandleColor:
selectionHandleColor === undefined
? selectionColor
: selectionHandleColor,
cursorColor: cursorColor === undefined ? selectionColor : cursorColor,
};
textInput = (
/* $FlowFixMe[prop-missing] the types for AndroidTextInput don't match up
* exactly with the props for TextInput. This will need to get fixed */
/* $FlowFixMe[incompatible-type] the types for AndroidTextInput don't
* match up exactly with the props for TextInput. This will need to get
* fixed */
/* $FlowFixMe[incompatible-type-arg] the types for AndroidTextInput don't
* match up exactly with the props for TextInput. This will need to get
* fixed */
<AndroidTextInput
// Figure out imperative + forward refs.
ref={(ref: $FlowFixMe)}
{...otherProps}
{...colorProps}
{...eventHandlers}
accessibilityLabel={_accessibilityLabel}
accessibilityLabelledBy={_accessibilityLabelledBy}
accessibilityState={_accessibilityState}
accessible={accessible}
acceptDragAndDropTypes={props.experimental_acceptDragAndDropTypes}
autoCapitalize={autoCapitalize}
submitBehavior={submitBehavior}
caretHidden={caretHidden}
children={children}
disableFullscreenUI={props.disableFullscreenUI}
focusable={tabIndex !== undefined ? !tabIndex : focusable}
importantForAccessibility={_importantForAccessibility}
mostRecentEventCount={mostRecentEventCount}
nativeID={id ?? props.nativeID}
numberOfLines={props.rows ?? props.numberOfLines}
onBlur={_onBlur}
onChange={_onChange}
onFocus={_onFocus}
/* $FlowFixMe[prop-missing] the types for AndroidTextInput don't match
* up exactly with the props for TextInput. This will need to get fixed
*/
/* $FlowFixMe[incompatible-type] the types for AndroidTextInput
* don't match up exactly with the props for TextInput. This will need
* to get fixed */
onScroll={_onScroll}
onSelectionChange={_onSelectionChange}
placeholder={placeholder}
style={_style}
text={text}
textBreakStrategy={props.textBreakStrategy}
/>
);
}
return <TextAncestorContext value={true}>{textInput}</TextAncestorContext>;
}
const enterKeyHintToReturnTypeMap = {
enter: 'default',
done: 'done',
go: 'go',
next: 'next',
previous: 'previous',
search: 'search',
send: 'send',
} as const;
const inputModeToKeyboardTypeMap = {
none: 'default',
text: 'default',
decimal: 'decimal-pad',
numeric: 'number-pad',
tel: 'phone-pad',
search:
Platform.OS === 'ios' ? ('web-search' as const) : ('default' as const),
email: 'email-address',
url: 'url',
} as const;
// Map HTML autocomplete values to Android autoComplete values
const autoCompleteWebToAutoCompleteAndroidMap = {
'address-line1': 'postal-address-region',
'address-line2': 'postal-address-locality',
bday: 'birthdate-full',
'bday-day': 'birthdate-day',
'bday-month': 'birthdate-month',
'bday-year': 'birthdate-year',
'cc-csc': 'cc-csc',
'cc-exp': 'cc-exp',
'cc-exp-month': 'cc-exp-month',
'cc-exp-year': 'cc-exp-year',
'cc-number': 'cc-number',
country: 'postal-address-country',
'current-password': 'password',
email: 'email',
'honorific-prefix': 'name-prefix',
'honorific-suffix': 'name-suffix',
name: 'name',
'additional-name': 'name-middle',
'family-name': 'name-family',
'given-name': 'name-given',
'new-password': 'password-new',
off: 'off',
'one-time-code': 'sms-otp',
'postal-code': 'postal-code',
sex: 'gender',
'street-address': 'street-address',
tel: 'tel',
'tel-country-code': 'tel-country-code',
'tel-national': 'tel-national',
username: 'username',
} as const;
// Map HTML autocomplete values to iOS textContentType values
const autoCompleteWebToTextContentTypeMap = {
'address-line1': 'streetAddressLine1',
'address-line2': 'streetAddressLine2',
bday: 'birthdate',
'bday-day': 'birthdateDay',
'bday-month': 'birthdateMonth',
'bday-year': 'birthdateYear',
'cc-csc': 'creditCardSecurityCode',
'cc-exp-month': 'creditCardExpirationMonth',
'cc-exp-year': 'creditCardExpirationYear',
'cc-exp': 'creditCardExpiration',
'cc-given-name': 'creditCardGivenName',
'cc-additional-name': 'creditCardMiddleName',
'cc-family-name': 'creditCardFamilyName',
'cc-name': 'creditCardName',
'cc-number': 'creditCardNumber',
'cc-type': 'creditCardType',
'current-password': 'password',
country: 'countryName',
email: 'emailAddress',
name: 'name',
'additional-name': 'middleName',
'family-name': 'familyName',
'given-name': 'givenName',
nickname: 'nickname',
'honorific-prefix': 'namePrefix',
'honorific-suffix': 'nameSuffix',
'new-password': 'newPassword',
off: 'none',
'one-time-code': 'oneTimeCode',
organization: 'organizationName',
'organization-title': 'jobTitle',
'postal-code': 'postalCode',
'street-address': 'fullStreetAddress',
tel: 'telephoneNumber',
url: 'URL',
username: 'username',
} as const;
const TextInput: component(
ref?: React.RefSetter<TextInputInstance>,
...props: React.ElementConfig<typeof InternalTextInput>
) = function TextInput({
ref: forwardedRef,
allowFontScaling = true,
rejectResponderTermination = true,
underlineColorAndroid = 'transparent',
autoComplete,
textContentType,
readOnly,
editable,
enterKeyHint,
returnKeyType,
inputMode,
showSoftInputOnFocus,
keyboardType,
...restProps
}: {
ref?: React.RefSetter<TextInputInstance>,
...React.ElementConfig<typeof InternalTextInput>,
}) {
return (
<InternalTextInput
allowFontScaling={allowFontScaling}
rejectResponderTermination={rejectResponderTermination}
underlineColorAndroid={underlineColorAndroid}
editable={readOnly !== undefined ? !readOnly : editable}
returnKeyType={
enterKeyHint ? enterKeyHintToReturnTypeMap[enterKeyHint] : returnKeyType
}
keyboardType={
inputMode ? inputModeToKeyboardTypeMap[inputMode] : keyboardType
}
showSoftInputOnFocus={
inputMode == null ? showSoftInputOnFocus : inputMode !== 'none'
}
autoComplete={
Platform.OS === 'android'
? // $FlowFixMe[invalid-computed-prop]
// $FlowFixMe[prop-missing]
(autoCompleteWebToAutoCompleteAndroidMap[autoComplete] ??
autoComplete)
: undefined
}
textContentType={
textContentType != null
? textContentType
: Platform.OS === 'ios' &&
autoComplete &&
autoComplete in autoCompleteWebToTextContentTypeMap
? // $FlowFixMe[prop-missing]
autoCompleteWebToTextContentTypeMap[autoComplete]
: textContentType
}
{...restProps}
forwardedRef={forwardedRef}
/>
);
};
TextInput.displayName = 'TextInput';
// $FlowFixMe[prop-missing]
TextInput.State = {
currentlyFocusedInput: TextInputState.currentlyFocusedInput,
currentlyFocusedField: TextInputState.currentlyFocusedField,
focusTextInput: TextInputState.focusTextInput,
blurTextInput: TextInputState.blurTextInput,
};
export type TextInputComponentStatics = $ReadOnly<{
State: TextInputStateType,
}>;
const styles = StyleSheet.create({
multilineDefault: {
// This default top inset makes RCTMultilineTextInputView seem as close as possible
// to single-line RCTSinglelineTextInputView defaults, using the system defaults
// of font size 17 and a height of 31 points.
paddingTop: 5,
},
});
const verticalAlignToTextAlignVerticalMap = {
auto: 'auto',
top: 'top',
bottom: 'bottom',
middle: 'center',
} as const;
// $FlowFixMe[unclear-type] Unclear type. Using `any` type is not safe.
export default TextInput as any as TextInputType;

View File

@@ -0,0 +1,29 @@
/**
* 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.
*
* @flow strict-local
* @format
*/
import type {Int32} from '../../Types/CodegenTypes';
import * as React from 'react';
export interface TextInputNativeCommands<T> {
+focus: (viewRef: React.ElementRef<T>) => void;
+blur: (viewRef: React.ElementRef<T>) => void;
+setTextAndSelection: (
viewRef: React.ElementRef<T>,
mostRecentEventCount: Int32,
value: ?string, // in theory this is nullable
start: Int32,
end: Int32,
) => void;
}
const supportedCommands = ['focus', 'blur', 'setTextAndSelection'] as string[];
export default supportedCommands;

View File

@@ -0,0 +1,198 @@
/**
* 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.
*
* @flow strict-local
* @format
*/
// This class is responsible for coordinating the "focused" state for
// TextInputs. All calls relating to the keyboard should be funneled
// through here.
import type {HostInstance} from '../../../src/private/types/HostInstance';
import {Commands as AndroidTextInputCommands} from '../../Components/TextInput/AndroidTextInputNativeComponent';
import {Commands as iOSTextInputCommands} from '../../Components/TextInput/RCTSingelineTextInputNativeComponent';
const {findNodeHandle} = require('../../ReactNative/RendererProxy');
const Platform = require('../../Utilities/Platform').default;
let currentlyFocusedInputRef: ?HostInstance = null;
const inputs = new Set<HostInstance>();
function currentlyFocusedInput(): ?HostInstance {
return currentlyFocusedInputRef;
}
/**
* Returns the ID of the currently focused text field, if one exists
* If no text field is focused it returns null
*/
function currentlyFocusedField(): ?number {
if (__DEV__) {
console.error(
'currentlyFocusedField is deprecated and will be removed in a future release. Use currentlyFocusedInput',
);
}
return findNodeHandle<$FlowFixMe>(currentlyFocusedInputRef);
}
function focusInput(textField: ?HostInstance): void {
if (currentlyFocusedInputRef !== textField && textField != null) {
currentlyFocusedInputRef = textField;
}
}
function blurInput(textField: ?HostInstance): void {
if (currentlyFocusedInputRef === textField && textField != null) {
currentlyFocusedInputRef = null;
}
}
function focusField(textFieldID: ?number): void {
if (__DEV__) {
console.error('focusField no longer works. Use focusInput');
}
return;
}
function blurField(textFieldID: ?number) {
if (__DEV__) {
console.error('blurField no longer works. Use blurInput');
}
return;
}
/**
* @param {number} TextInputID id of the text field to focus
* Focuses the specified text field
* noop if the text field was already focused or if the field is not editable
*/
function focusTextInput(textField: ?HostInstance) {
if (typeof textField === 'number') {
if (__DEV__) {
console.error(
'focusTextInput must be called with a host component. Passing a react tag is deprecated.',
);
}
return;
}
if (textField != null) {
const fieldCanBeFocused =
currentlyFocusedInputRef !== textField &&
// $FlowFixMe[prop-missing] - `currentProps` is missing in `NativeMethods`
textField.currentProps?.editable !== false;
if (!fieldCanBeFocused) {
return;
}
focusInput(textField);
if (Platform.OS === 'ios') {
// This isn't necessarily a single line text input
// But commands don't actually care as long as the thing being passed in
// actually has a command with that name. So this should work with single
// and multiline text inputs. Ideally we'll merge them into one component
// in the future.
iOSTextInputCommands.focus(textField);
} else if (Platform.OS === 'android') {
AndroidTextInputCommands.focus(textField);
}
}
}
/**
* @param {number} textFieldID id of the text field to unfocus
* Unfocuses the specified text field
* noop if it wasn't focused
*/
function blurTextInput(textField: ?HostInstance) {
if (typeof textField === 'number') {
if (__DEV__) {
console.error(
'blurTextInput must be called with a host component. Passing a react tag is deprecated.',
);
}
return;
}
if (currentlyFocusedInputRef === textField && textField != null) {
blurInput(textField);
if (Platform.OS === 'ios') {
// This isn't necessarily a single line text input
// But commands don't actually care as long as the thing being passed in
// actually has a command with that name. So this should work with single
// and multiline text inputs. Ideally we'll merge them into one component
// in the future.
iOSTextInputCommands.blur(textField);
} else if (Platform.OS === 'android') {
AndroidTextInputCommands.blur(textField);
}
}
}
function registerInput(textField: HostInstance) {
if (typeof textField === 'number') {
if (__DEV__) {
console.error(
'registerInput must be called with a host component. Passing a react tag is deprecated.',
);
}
return;
}
inputs.add(textField);
}
function unregisterInput(textField: HostInstance) {
if (typeof textField === 'number') {
if (__DEV__) {
console.error(
'unregisterInput must be called with a host component. Passing a react tag is deprecated.',
);
}
return;
}
inputs.delete(textField);
}
function isTextInput(textField: HostInstance): boolean {
if (typeof textField === 'number') {
if (__DEV__) {
console.error(
'isTextInput must be called with a host component. Passing a react tag is deprecated.',
);
}
return false;
}
return inputs.has(textField);
}
const TextInputState = {
currentlyFocusedInput,
focusInput,
blurInput,
currentlyFocusedField,
focusField,
blurField,
focusTextInput,
blurTextInput,
registerInput,
unregisterInput,
isTextInput,
};
export default TextInputState;

View File

@@ -0,0 +1,12 @@
/**
* 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.
*
* @flow strict
* @format
*/
export * from '../../../src/private/specs_DEPRECATED/modules/NativeToastAndroid';
export {default} from '../../../src/private/specs_DEPRECATED/modules/NativeToastAndroid';

View File

@@ -0,0 +1,74 @@
/**
* 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.
*
* @flow strict-local
* @format
*/
import NativeToastAndroid from './NativeToastAndroid';
/**
* This exposes the native ToastAndroid module as a JS module. This has a function 'show'
* which takes the following parameters:
*
* 1. String message: A string with the text to toast
* 2. int duration: The duration of the toast. May be ToastAndroid.SHORT or ToastAndroid.LONG
*
* There is also a function `showWithGravity` to specify the layout gravity. May be
* ToastAndroid.TOP, ToastAndroid.BOTTOM, ToastAndroid.CENTER.
*
* The 'showWithGravityAndOffset' function adds on the ability to specify offset
* These offset values will translate to pixels.
*
* Basic usage:
* ```javascript
* ToastAndroid.show('A pikachu appeared nearby !', ToastAndroid.SHORT);
* ToastAndroid.showWithGravity('All Your Base Are Belong To Us', ToastAndroid.SHORT, ToastAndroid.CENTER);
* ToastAndroid.showWithGravityAndOffset('A wild toast appeared!', ToastAndroid.LONG, ToastAndroid.BOTTOM, 25, 50);
* ```
*/
const ToastAndroidConstants = NativeToastAndroid.getConstants();
const ToastAndroid = {
// Toast duration constants
SHORT: (ToastAndroidConstants.SHORT: number),
LONG: (ToastAndroidConstants.LONG: number),
// Toast gravity constants
TOP: (ToastAndroidConstants.TOP: number),
BOTTOM: (ToastAndroidConstants.BOTTOM: number),
CENTER: (ToastAndroidConstants.CENTER: number),
show: function (message: string, duration: number): void {
NativeToastAndroid.show(message, duration);
},
showWithGravity: function (
message: string,
duration: number,
gravity: number,
): void {
NativeToastAndroid.showWithGravity(message, duration, gravity);
},
showWithGravityAndOffset: function (
message: string,
duration: number,
gravity: number,
xOffset: number,
yOffset: number,
): void {
NativeToastAndroid.showWithGravityAndOffset(
message,
duration,
gravity,
xOffset,
yOffset,
);
},
};
export default ToastAndroid;

View File

@@ -0,0 +1,109 @@
/**
* 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
*/
/**
* This exposes the native ToastAndroid module as a JS module. This has a function 'show'
* which takes the following parameters:
*
* 1. String message: A string with the text to toast
* 2. int duration: The duration of the toast. May be ToastAndroid.SHORT or ToastAndroid.LONG
*
* There is also a function `showWithGravity` to specify the layout gravity. May be
* ToastAndroid.TOP, ToastAndroid.BOTTOM, ToastAndroid.CENTER
*
* **Note**: Starting from Android API level 30 (Android R) or higher, for apps targeting
* that API level, setting toast gravity is a no-op for text toasts.
* This means that in many cases `TOP`, `BOTTOM`, `CENTER`, or offsets may not have
* any visible effect on actual toast positioning.
*
* Reference: https://developer.android.com/reference/android/widget/Toast#setGravity(int,%20int,%20int)
*/
export interface ToastAndroidStatic {
/**
* Display a toast message for a specified duration.
*
* @param message A string with the text to toast.
* @param duration The duration of the toasteither ToastAndroid.SHORT or ToastAndroid.LONG
*/
show(message: string, duration: number): void;
/**
* Display a toast message for a specified duration with a given gravity.
*
* @param message A string with the text to display in the toast.
* @param duration The duration of the toast.
* May be `ToastAndroid.SHORT` or `ToastAndroid.LONG`.
* @param gravity Positioning on the screen, e.g.,
* `ToastAndroid.TOP`, `ToastAndroid.BOTTOM`, or `ToastAndroid.CENTER`.
*
* **Note**: On Android R (API 30) or later (when targeting API 30+), this setting may
* not have any effect on text toast placement due to `setGravity` becoming a no-op.
*/
showWithGravity(message: string, duration: number, gravity: number): void;
/**
* Display a toast message for a specified duration with a given gravity and custom offsets.
*
* @param message A string with the text to display in the toast.
* @param duration The duration of the toast.
* May be `ToastAndroid.SHORT` or `ToastAndroid.LONG`.
* @param gravity Positioning on the screen, e.g.,
* `ToastAndroid.TOP`, `ToastAndroid.BOTTOM`, or `ToastAndroid.CENTER`.
* @param xOffset Horizontal offset from the given gravity.
* @param yOffset Vertical offset from the given gravity.
*
* **Note**: On Android R (API 30) or later (when targeting API 30+), setting gravity
* and offsets may not visibly affect the placement of text toasts.
*/
showWithGravityAndOffset(
message: string,
duration: number,
gravity: number,
xOffset: number,
yOffset: number,
): void;
/**
* Indicates a short duration on the screen.
*
* Value: 2000 milliseconds (2 seconds).
*/
SHORT: number;
/**
* Indicates a long duration on the screen.
*
* Value: 3500 milliseconds (3.5 seconds).
*/
LONG: number;
/**
* Indicates that the toast message should appear at the top of the screen.
*
* **Note**: On Android R or later, this may not have any visible effect.
*/
TOP: number;
/**
* Indicates that the toast message should appear at the bottom of the screen.
*
* **Note**: On Android R or later, this may not have any visible effect.
*/
BOTTOM: number;
/**
* Indicates that the toast message should appear at the center of the screen.
*
* **Note**: On Android R or later, this may not have any visible effect.
*/
CENTER: number;
}
export const ToastAndroid: ToastAndroidStatic;
export type ToastAndroid = ToastAndroidStatic;

View File

@@ -0,0 +1,13 @@
/**
* 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.
*
* @flow strict-local
* @format
*/
import ToastAndroidFallback from './ToastAndroidFallback';
export default ToastAndroidFallback;

View File

@@ -0,0 +1,17 @@
/**
* 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.
*
* @flow strict-local
* @format
*/
// NOTE: This file supports backwards compatibility of subpath (deep) imports
// from 'react-native' with platform-specific extensions. It can be deleted
// once we remove the "./*" mapping from package.json "exports".
import ToastAndroid from './ToastAndroid';
export default ToastAndroid;

View File

@@ -0,0 +1,109 @@
/**
* 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.
*
* @flow strict-local
* @format
*/
/**
* This exposes the native ToastAndroid module as a JS module. This has a function 'show'
* which takes the following parameters:
*
* 1. String message: A string with the text to toast
* 2. int duration: The duration of the toast. May be ToastAndroid.SHORT or ToastAndroid.LONG
*
* There is also a function `showWithGravity` to specify the layout gravity. May be
* ToastAndroid.TOP, ToastAndroid.BOTTOM, ToastAndroid.CENTER
*
* **Note**: Starting from Android API level 30 (Android R) or higher, for apps targeting
* that API level, setting toast gravity is a no-op for text toasts.
* This means that in many cases `TOP`, `BOTTOM`, `CENTER`, or offsets may not have
* any visible effect on actual toast positioning.
*
* Reference: https://developer.android.com/reference/android/widget/Toast#setGravity(int,%20int,%20int)
*/
declare const ToastAndroid: {
/**
* Indicates a short duration on the screen.
*
* Value: 2000 milliseconds (2 seconds).
*/
SHORT: number,
/**
* Indicates a long duration on the screen.
*
* Value: 3500 milliseconds (3.5 seconds).
*/
LONG: number,
/**
* Indicates that the toast message should appear at the top of the screen.
*
* **Note**: On Android R or later, this may not have any visible effect.
*/
TOP: number,
/**
* Indicates that the toast message should appear at the bottom of the screen.
*
* **Note**: On Android R or later, this may not have any visible effect.
*/
BOTTOM: number,
/**
* Indicates that the toast message should appear at the center of the screen.
*
* **Note**: On Android R or later, this may not have any visible effect.
*/
CENTER: number,
/**
* Display a toast message for a specified duration.
*
* @param message A string with the text to toast.
* @param duration The duration of the toasteither ToastAndroid.SHORT or ToastAndroid.LONG
*/
show: (message: string, duration: number) => void,
/**
* Display a toast message for a specified duration with a given gravity.
*
* @param message A string with the text to display in the toast.
* @param duration The duration of the toast.
* May be `ToastAndroid.SHORT` or `ToastAndroid.LONG`.
* @param gravity Positioning on the screen, e.g.,
* `ToastAndroid.TOP`, `ToastAndroid.BOTTOM`, or `ToastAndroid.CENTER`.
*
* **Note**: On Android R (API 30) or later (when targeting API 30+), this setting may
* not have any effect on text toast placement due to `setGravity` becoming a no-op.
*/
showWithGravity: (message: string, duration: number, gravity: number) => void,
/**
* Display a toast message for a specified duration with a given gravity and custom offsets.
*
* @param message A string with the text to display in the toast.
* @param duration The duration of the toast.
* May be `ToastAndroid.SHORT` or `ToastAndroid.LONG`.
* @param gravity Positioning on the screen, e.g.,
* `ToastAndroid.TOP`, `ToastAndroid.BOTTOM`, or `ToastAndroid.CENTER`.
* @param xOffset Horizontal offset from the given gravity.
* @param yOffset Vertical offset from the given gravity.
*
* **Note**: On Android R (API 30) or later (when targeting API 30+), setting gravity
* and offsets may not visibly affect the placement of text toasts.
*/
showWithGravityAndOffset: (
message: string,
duration: number,
gravity: number,
xOffset: number,
yOffset: number,
) => void,
};
export default ToastAndroid;

View File

@@ -0,0 +1,45 @@
/**
* 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.
*
* @flow strict-local
* @format
*/
'use strict';
const ToastAndroid = {
// Dummy fallback toast duration constants
SHORT: (0: number),
LONG: (0: number),
// Dummy fallback toast gravity constants
TOP: (0: number),
BOTTOM: (0: number),
CENTER: (0: number),
show: function (message: string, duration: number): void {
console.warn('ToastAndroid is not supported on this platform.');
},
showWithGravity: function (
message: string,
duration: number,
gravity: number,
): void {
console.warn('ToastAndroid is not supported on this platform.');
},
showWithGravityAndOffset: function (
message: string,
duration: number,
gravity: number,
xOffset: number,
yOffset: number,
): void {
console.warn('ToastAndroid is not supported on this platform.');
},
};
export default ToastAndroid;

View File

@@ -0,0 +1,53 @@
/**
* 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.
*
* @flow strict-local
* @format
*/
'use strict';
import PooledClass from './PooledClass';
const twoArgumentPooler = PooledClass.twoArgumentPooler;
/**
* PooledClass representing the bounding rectangle of a region.
*
* @param {number} width Width of bounding rectangle.
* @param {number} height Height of bounding rectangle.
* @constructor BoundingDimensions
*/
// $FlowFixMe[missing-this-annot]
function BoundingDimensions(width: number, height: number) {
this.width = width;
this.height = height;
}
// $FlowFixMe[prop-missing]
// $FlowFixMe[missing-this-annot]
BoundingDimensions.prototype.destructor = function () {
this.width = null;
this.height = null;
};
/**
* @param {HTMLElement} element Element to return `BoundingDimensions` for.
* @return {BoundingDimensions} Bounding dimensions of `element`.
*/
BoundingDimensions.getPooledFromElement = function (
element: HTMLElement,
): typeof BoundingDimensions {
// $FlowFixMe[prop-missing]
return BoundingDimensions.getPooled(
element.offsetWidth,
element.offsetHeight,
);
};
PooledClass.addPoolingTo(BoundingDimensions as $FlowFixMe, twoArgumentPooler);
export default BoundingDimensions;

View File

@@ -0,0 +1,133 @@
/**
* 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.
*
* @flow
* @format
*/
'use strict';
import invariant from 'invariant';
/**
* Static poolers. Several custom versions for each potential number of
* arguments. A completely generic pooler is easy to implement, but would
* require accessing the `arguments` object. In each of these, `this` refers to
* the Class itself, not an instance. If any others are needed, simply add them
* here, or in their own files.
*/
/* $FlowFixMe[missing-this-annot] The 'this' type annotation(s) required by
* Flow's LTI update could not be added via codemod */
const oneArgumentPooler = function (copyFieldsFrom: any) {
const Klass = this; // eslint-disable-line consistent-this
if (Klass.instancePool.length) {
const instance = Klass.instancePool.pop();
Klass.call(instance, copyFieldsFrom);
return instance;
} else {
return new Klass(copyFieldsFrom);
}
};
/* $FlowFixMe[missing-this-annot] The 'this' type annotation(s) required by
* Flow's LTI update could not be added via codemod */
const twoArgumentPooler = function (a1: any, a2: any) {
const Klass = this; // eslint-disable-line consistent-this
if (Klass.instancePool.length) {
const instance = Klass.instancePool.pop();
Klass.call(instance, a1, a2);
return instance;
} else {
return new Klass(a1, a2);
}
};
/* $FlowFixMe[missing-this-annot] The 'this' type annotation(s) required by
* Flow's LTI update could not be added via codemod */
const threeArgumentPooler = function (a1: any, a2: any, a3: any) {
const Klass = this; // eslint-disable-line consistent-this
if (Klass.instancePool.length) {
const instance = Klass.instancePool.pop();
Klass.call(instance, a1, a2, a3);
return instance;
} else {
return new Klass(a1, a2, a3);
}
};
/* $FlowFixMe[missing-this-annot] The 'this' type annotation(s) required by
* Flow's LTI update could not be added via codemod */
const fourArgumentPooler = function (a1: any, a2: any, a3: any, a4: any) {
const Klass = this; // eslint-disable-line consistent-this
if (Klass.instancePool.length) {
const instance = Klass.instancePool.pop();
Klass.call(instance, a1, a2, a3, a4);
return instance;
} else {
return new Klass(a1, a2, a3, a4);
}
};
/* $FlowFixMe[missing-local-annot] The type annotation(s) required by Flow's
* LTI update could not be added via codemod */
/* $FlowFixMe[missing-this-annot] The 'this' type annotation(s) required by
* Flow's LTI update could not be added via codemod */
const standardReleaser = function (instance) {
const Klass = this; // eslint-disable-line consistent-this
invariant(
instance instanceof Klass,
'Trying to release an instance into a pool of a different type.',
);
instance.destructor();
if (Klass.instancePool.length < Klass.poolSize) {
Klass.instancePool.push(instance);
}
};
const DEFAULT_POOL_SIZE = 10;
const DEFAULT_POOLER = oneArgumentPooler;
type Pooler = any;
/**
* Augments `CopyConstructor` to be a poolable class, augmenting only the class
* itself (statically) not adding any prototypical fields. Any CopyConstructor
* you give this may have a `poolSize` property, and will look for a
* prototypical `destructor` on instances.
*
* @param {Function} CopyConstructor Constructor that can be used to reset.
* @param {Function} pooler Customizable pooler.
*/
const addPoolingTo = function <T>(
CopyConstructor: Class<T>,
pooler: Pooler,
): Class<T> & {
getPooled(
...args: $ReadOnlyArray<mixed>
): /* arguments of the constructor */ T,
release(instance: mixed): void,
...
} {
// Casting as any so that flow ignores the actual implementation and trusts
// it to match the type we declared
const NewKlass: any = CopyConstructor;
NewKlass.instancePool = [];
NewKlass.getPooled = pooler || DEFAULT_POOLER;
if (!NewKlass.poolSize) {
NewKlass.poolSize = DEFAULT_POOL_SIZE;
}
NewKlass.release = standardReleaser;
return NewKlass;
};
const PooledClass = {
addPoolingTo: addPoolingTo,
oneArgumentPooler: (oneArgumentPooler: Pooler),
twoArgumentPooler: (twoArgumentPooler: Pooler),
threeArgumentPooler: (threeArgumentPooler: Pooler),
fourArgumentPooler: (fourArgumentPooler: Pooler),
};
export default PooledClass;

View File

@@ -0,0 +1,40 @@
/**
* 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.
*
* @flow strict-local
* @format
*/
'use strict';
import PooledClass from './PooledClass';
const twoArgumentPooler = PooledClass.twoArgumentPooler;
/**
* Position does not expose methods for construction via an `HTMLDOMElement`,
* because it isn't meaningful to construct such a thing without first defining
* a frame of reference.
*
* @param {number} windowStartKey Key that window starts at.
* @param {number} windowEndKey Key that window ends at.
*/
// $FlowFixMe[missing-this-annot]
function Position(left: number, top: number) {
this.left = left;
this.top = top;
}
// $FlowFixMe[prop-missing]
// $FlowFixMe[missing-this-annot]
Position.prototype.destructor = function () {
this.left = null;
this.top = null;
};
PooledClass.addPoolingTo(Position as $FlowFixMe, twoArgumentPooler);
export default Position;

View File

@@ -0,0 +1,90 @@
/**
* 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
*/
import type * as React from 'react';
import {Insets} from '../../../types/public/Insets';
import {GestureResponderEvent} from '../../Types/CoreEventTypes';
/**
* //FIXME: need to find documentation on which component is a TTouchable and can implement that interface
* @see React.DOMAttributes
*/
export interface Touchable {
onTouchStart?: ((event: GestureResponderEvent) => void) | undefined;
onTouchMove?: ((event: GestureResponderEvent) => void) | undefined;
onTouchEnd?: ((event: GestureResponderEvent) => void) | undefined;
onTouchCancel?: ((event: GestureResponderEvent) => void) | undefined;
onTouchEndCapture?: ((event: GestureResponderEvent) => void) | undefined;
}
export const Touchable: {
TOUCH_TARGET_DEBUG: boolean;
renderDebugView: (config: {
color: string | number;
hitSlop?: Insets | undefined;
}) => React.ReactElement | null;
};
/**
* @see https://github.com/facebook/react-native/blob/0.34-stable\Libraries\Components\Touchable\Touchable.js
*/
interface TouchableMixin {
/**
* Invoked when the item should be highlighted. Mixers should implement this
* to visually distinguish the `VisualRect` so that the user knows that
* releasing a touch will result in a "selection" (analog to click).
*/
touchableHandleActivePressIn(e: GestureResponderEvent): void;
/**
* Invoked when the item is "active" (in that it is still eligible to become
* a "select") but the touch has left the `PressRect`. Usually the mixer will
* want to unhighlight the `VisualRect`. If the user (while pressing) moves
* back into the `PressRect` `touchableHandleActivePressIn` will be invoked
* again and the mixer should probably highlight the `VisualRect` again. This
* event will not fire on an `touchEnd/mouseUp` event, only move events while
* the user is depressing the mouse/touch.
*/
touchableHandleActivePressOut(e: GestureResponderEvent): void;
/**
* Invoked when the item is "selected" - meaning the interaction ended by
* letting up while the item was either in the state
* `RESPONDER_ACTIVE_PRESS_IN` or `RESPONDER_INACTIVE_PRESS_IN`.
*/
touchableHandlePress(e: GestureResponderEvent): void;
/**
* Invoked when the item is long pressed - meaning the interaction ended by
* letting up while the item was in `RESPONDER_ACTIVE_LONG_PRESS_IN`. If
* `touchableHandleLongPress` is *not* provided, `touchableHandlePress` will
* be called as it normally is. If `touchableHandleLongPress` is provided, by
* default any `touchableHandlePress` callback will not be invoked. To
* override this default behavior, override `touchableLongPressCancelsPress`
* to return false. As a result, `touchableHandlePress` will be called when
* lifting up, even if `touchableHandleLongPress` has also been called.
*/
touchableHandleLongPress(e: GestureResponderEvent): void;
/**
* Returns the amount to extend the `HitRect` into the `PressRect`. Positive
* numbers mean the size expands outwards.
*/
touchableGetPressRectOffset(): Insets;
/**
* Returns the number of millis to wait before triggering a highlight.
*/
touchableGetHighlightDelayMS(): number;
// These methods are undocumented but still being used by TouchableMixin internals
touchableGetLongPressDelayMS(): number;
touchableGetPressOutDelayMS(): number;
touchableGetHitSlop(): Insets;
}

View File

@@ -0,0 +1,987 @@
/**
* 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.
*
* @flow
* @format
*/
import type {EdgeInsetsProp} from '../../StyleSheet/EdgeInsetsPropType';
import type {ColorValue} from '../../StyleSheet/StyleSheet';
import type {
BlurEvent,
FocusEvent,
GestureResponderEvent,
} from '../../Types/CoreEventTypes';
import {PressabilityDebugView} from '../../Pressability/PressabilityDebug';
import UIManager from '../../ReactNative/UIManager';
import Platform from '../../Utilities/Platform';
import SoundManager from '../Sound/SoundManager';
import BoundingDimensions from './BoundingDimensions';
import Position from './Position';
import * as React from 'react';
const extractSingleTouch = (nativeEvent: {
+changedTouches: $ReadOnlyArray<GestureResponderEvent['nativeEvent']>,
+force?: number,
+identifier: number,
+locationX: number,
+locationY: number,
+pageX: number,
+pageY: number,
+target: ?number,
+timestamp: number,
+touches: $ReadOnlyArray<GestureResponderEvent['nativeEvent']>,
}) => {
const touches = nativeEvent.touches;
const changedTouches = nativeEvent.changedTouches;
const hasTouches = touches && touches.length > 0;
const hasChangedTouches = changedTouches && changedTouches.length > 0;
return !hasTouches && hasChangedTouches
? changedTouches[0]
: hasTouches
? touches[0]
: nativeEvent;
};
/**
* `Touchable`: Taps done right.
*
* You hook your `ResponderEventPlugin` events into `Touchable`. `Touchable`
* will measure time/geometry and tells you when to give feedback to the user.
*
* ====================== Touchable Tutorial ===============================
* The `Touchable` mixin helps you handle the "press" interaction. It analyzes
* the geometry of elements, and observes when another responder (scroll view
* etc) has stolen the touch lock. It notifies your component when it should
* give feedback to the user. (bouncing/highlighting/unhighlighting).
*
* - When a touch was activated (typically you highlight)
* - When a touch was deactivated (typically you unhighlight)
* - When a touch was "pressed" - a touch ended while still within the geometry
* of the element, and no other element (like scroller) has "stolen" touch
* lock ("responder") (Typically you bounce the element).
*
* A good tap interaction isn't as simple as you might think. There should be a
* slight delay before showing a highlight when starting a touch. If a
* subsequent touch move exceeds the boundary of the element, it should
* unhighlight, but if that same touch is brought back within the boundary, it
* should rehighlight again. A touch can move in and out of that boundary
* several times, each time toggling highlighting, but a "press" is only
* triggered if that touch ends while within the element's boundary and no
* scroller (or anything else) has stolen the lock on touches.
*
* To create a new type of component that handles interaction using the
* `Touchable` mixin, do the following:
*
* - Initialize the `Touchable` state.
*
* getInitialState: function() {
* return merge(this.touchableGetInitialState(), yourComponentState);
* }
*
* - Choose the rendered component who's touches should start the interactive
* sequence. On that rendered node, forward all `Touchable` responder
* handlers. You can choose any rendered node you like. Choose a node whose
* hit target you'd like to instigate the interaction sequence:
*
* // In render function:
* return (
* <View
* onStartShouldSetResponder={this.touchableHandleStartShouldSetResponder}
* onResponderTerminationRequest={this.touchableHandleResponderTerminationRequest}
* onResponderGrant={this.touchableHandleResponderGrant}
* onResponderMove={this.touchableHandleResponderMove}
* onResponderRelease={this.touchableHandleResponderRelease}
* onResponderTerminate={this.touchableHandleResponderTerminate}>
* <View>
* Even though the hit detection/interactions are triggered by the
* wrapping (typically larger) node, we usually end up implementing
* custom logic that highlights this inner one.
* </View>
* </View>
* );
*
* - You may set up your own handlers for each of these events, so long as you
* also invoke the `touchable*` handlers inside of your custom handler.
*
* - Implement the handlers on your component class in order to provide
* feedback to the user. See documentation for each of these class methods
* that you should implement.
*
* touchableHandlePress: function() {
* this.performBounceAnimation(); // or whatever you want to do.
* },
* touchableHandleActivePressIn: function() {
* this.beginHighlighting(...); // Whatever you like to convey activation
* },
* touchableHandleActivePressOut: function() {
* this.endHighlighting(...); // Whatever you like to convey deactivation
* },
*
* - There are more advanced methods you can implement (see documentation below):
* touchableGetHighlightDelayMS: function() {
* return 20;
* }
* // In practice, *always* use a predeclared constant (conserve memory).
* touchableGetPressRectOffset: function() {
* return {top: 20, left: 20, right: 20, bottom: 100};
* }
*/
/**
* Touchable states.
*/
const States = {
NOT_RESPONDER: 'NOT_RESPONDER', // Not the responder
RESPONDER_INACTIVE_PRESS_IN: 'RESPONDER_INACTIVE_PRESS_IN', // Responder, inactive, in the `PressRect`
RESPONDER_INACTIVE_PRESS_OUT: 'RESPONDER_INACTIVE_PRESS_OUT', // Responder, inactive, out of `PressRect`
RESPONDER_ACTIVE_PRESS_IN: 'RESPONDER_ACTIVE_PRESS_IN', // Responder, active, in the `PressRect`
RESPONDER_ACTIVE_PRESS_OUT: 'RESPONDER_ACTIVE_PRESS_OUT', // Responder, active, out of `PressRect`
RESPONDER_ACTIVE_LONG_PRESS_IN: 'RESPONDER_ACTIVE_LONG_PRESS_IN', // Responder, active, in the `PressRect`, after long press threshold
RESPONDER_ACTIVE_LONG_PRESS_OUT: 'RESPONDER_ACTIVE_LONG_PRESS_OUT', // Responder, active, out of `PressRect`, after long press threshold
ERROR: 'ERROR',
};
type TouchableState =
| typeof States.NOT_RESPONDER
| typeof States.RESPONDER_INACTIVE_PRESS_IN
| typeof States.RESPONDER_INACTIVE_PRESS_OUT
| typeof States.RESPONDER_ACTIVE_PRESS_IN
| typeof States.RESPONDER_ACTIVE_PRESS_OUT
| typeof States.RESPONDER_ACTIVE_LONG_PRESS_IN
| typeof States.RESPONDER_ACTIVE_LONG_PRESS_OUT
| typeof States.ERROR;
/*
* Quick lookup map for states that are considered to be "active"
*/
const baseStatesConditions = {
NOT_RESPONDER: false,
RESPONDER_INACTIVE_PRESS_IN: false,
RESPONDER_INACTIVE_PRESS_OUT: false,
RESPONDER_ACTIVE_PRESS_IN: false,
RESPONDER_ACTIVE_PRESS_OUT: false,
RESPONDER_ACTIVE_LONG_PRESS_IN: false,
RESPONDER_ACTIVE_LONG_PRESS_OUT: false,
ERROR: false,
};
const IsActive = {
...baseStatesConditions,
RESPONDER_ACTIVE_PRESS_OUT: true,
RESPONDER_ACTIVE_PRESS_IN: true,
};
/**
* Quick lookup for states that are considered to be "pressing" and are
* therefore eligible to result in a "selection" if the press stops.
*/
const IsPressingIn = {
...baseStatesConditions,
RESPONDER_INACTIVE_PRESS_IN: true,
RESPONDER_ACTIVE_PRESS_IN: true,
RESPONDER_ACTIVE_LONG_PRESS_IN: true,
};
const IsLongPressingIn = {
...baseStatesConditions,
RESPONDER_ACTIVE_LONG_PRESS_IN: true,
};
/**
* Inputs to the state machine.
*/
const Signals = {
DELAY: 'DELAY',
RESPONDER_GRANT: 'RESPONDER_GRANT',
RESPONDER_RELEASE: 'RESPONDER_RELEASE',
RESPONDER_TERMINATED: 'RESPONDER_TERMINATED',
ENTER_PRESS_RECT: 'ENTER_PRESS_RECT',
LEAVE_PRESS_RECT: 'LEAVE_PRESS_RECT',
LONG_PRESS_DETECTED: 'LONG_PRESS_DETECTED',
};
type Signal =
| typeof Signals.DELAY
| typeof Signals.RESPONDER_GRANT
| typeof Signals.RESPONDER_RELEASE
| typeof Signals.RESPONDER_TERMINATED
| typeof Signals.ENTER_PRESS_RECT
| typeof Signals.LEAVE_PRESS_RECT
| typeof Signals.LONG_PRESS_DETECTED;
/**
* Mapping from States x Signals => States
*/
const Transitions = {
NOT_RESPONDER: {
DELAY: States.ERROR,
RESPONDER_GRANT: States.RESPONDER_INACTIVE_PRESS_IN,
RESPONDER_RELEASE: States.ERROR,
RESPONDER_TERMINATED: States.ERROR,
ENTER_PRESS_RECT: States.ERROR,
LEAVE_PRESS_RECT: States.ERROR,
LONG_PRESS_DETECTED: States.ERROR,
},
RESPONDER_INACTIVE_PRESS_IN: {
DELAY: States.RESPONDER_ACTIVE_PRESS_IN,
RESPONDER_GRANT: States.ERROR,
RESPONDER_RELEASE: States.NOT_RESPONDER,
RESPONDER_TERMINATED: States.NOT_RESPONDER,
ENTER_PRESS_RECT: States.RESPONDER_INACTIVE_PRESS_IN,
LEAVE_PRESS_RECT: States.RESPONDER_INACTIVE_PRESS_OUT,
LONG_PRESS_DETECTED: States.ERROR,
},
RESPONDER_INACTIVE_PRESS_OUT: {
DELAY: States.RESPONDER_ACTIVE_PRESS_OUT,
RESPONDER_GRANT: States.ERROR,
RESPONDER_RELEASE: States.NOT_RESPONDER,
RESPONDER_TERMINATED: States.NOT_RESPONDER,
ENTER_PRESS_RECT: States.RESPONDER_INACTIVE_PRESS_IN,
LEAVE_PRESS_RECT: States.RESPONDER_INACTIVE_PRESS_OUT,
LONG_PRESS_DETECTED: States.ERROR,
},
RESPONDER_ACTIVE_PRESS_IN: {
DELAY: States.ERROR,
RESPONDER_GRANT: States.ERROR,
RESPONDER_RELEASE: States.NOT_RESPONDER,
RESPONDER_TERMINATED: States.NOT_RESPONDER,
ENTER_PRESS_RECT: States.RESPONDER_ACTIVE_PRESS_IN,
LEAVE_PRESS_RECT: States.RESPONDER_ACTIVE_PRESS_OUT,
LONG_PRESS_DETECTED: States.RESPONDER_ACTIVE_LONG_PRESS_IN,
},
RESPONDER_ACTIVE_PRESS_OUT: {
DELAY: States.ERROR,
RESPONDER_GRANT: States.ERROR,
RESPONDER_RELEASE: States.NOT_RESPONDER,
RESPONDER_TERMINATED: States.NOT_RESPONDER,
ENTER_PRESS_RECT: States.RESPONDER_ACTIVE_PRESS_IN,
LEAVE_PRESS_RECT: States.RESPONDER_ACTIVE_PRESS_OUT,
LONG_PRESS_DETECTED: States.ERROR,
},
RESPONDER_ACTIVE_LONG_PRESS_IN: {
DELAY: States.ERROR,
RESPONDER_GRANT: States.ERROR,
RESPONDER_RELEASE: States.NOT_RESPONDER,
RESPONDER_TERMINATED: States.NOT_RESPONDER,
ENTER_PRESS_RECT: States.RESPONDER_ACTIVE_LONG_PRESS_IN,
LEAVE_PRESS_RECT: States.RESPONDER_ACTIVE_LONG_PRESS_OUT,
LONG_PRESS_DETECTED: States.RESPONDER_ACTIVE_LONG_PRESS_IN,
},
RESPONDER_ACTIVE_LONG_PRESS_OUT: {
DELAY: States.ERROR,
RESPONDER_GRANT: States.ERROR,
RESPONDER_RELEASE: States.NOT_RESPONDER,
RESPONDER_TERMINATED: States.NOT_RESPONDER,
ENTER_PRESS_RECT: States.RESPONDER_ACTIVE_LONG_PRESS_IN,
LEAVE_PRESS_RECT: States.RESPONDER_ACTIVE_LONG_PRESS_OUT,
LONG_PRESS_DETECTED: States.ERROR,
},
error: {
DELAY: States.NOT_RESPONDER,
RESPONDER_GRANT: States.RESPONDER_INACTIVE_PRESS_IN,
RESPONDER_RELEASE: States.NOT_RESPONDER,
RESPONDER_TERMINATED: States.NOT_RESPONDER,
ENTER_PRESS_RECT: States.NOT_RESPONDER,
LEAVE_PRESS_RECT: States.NOT_RESPONDER,
LONG_PRESS_DETECTED: States.NOT_RESPONDER,
},
};
// ==== Typical Constants for integrating into UI components ====
// var HIT_EXPAND_PX = 20;
// var HIT_VERT_OFFSET_PX = 10;
const HIGHLIGHT_DELAY_MS = 130;
const PRESS_EXPAND_PX = 20;
const LONG_PRESS_THRESHOLD = 500;
const LONG_PRESS_DELAY_MS = LONG_PRESS_THRESHOLD - HIGHLIGHT_DELAY_MS;
const LONG_PRESS_ALLOWED_MOVEMENT = 10;
// Default amount "active" region protrudes beyond box
/**
* By convention, methods prefixed with underscores are meant to be @private,
* and not @protected. Mixers shouldn't access them - not even to provide them
* as callback handlers.
*
*
* ========== Geometry =========
* `Touchable` only assumes that there exists a `HitRect` node. The `PressRect`
* is an abstract box that is extended beyond the `HitRect`.
*
* +--------------------------+
* | | - "Start" events in `HitRect` cause `HitRect`
* | +--------------------+ | to become the responder.
* | | +--------------+ | | - `HitRect` is typically expanded around
* | | | | | | the `VisualRect`, but shifted downward.
* | | | VisualRect | | | - After pressing down, after some delay,
* | | | | | | and before letting up, the Visual React
* | | +--------------+ | | will become "active". This makes it eligible
* | | HitRect | | for being highlighted (so long as the
* | +--------------------+ | press remains in the `PressRect`).
* | PressRect o |
* +----------------------|---+
* Out Region |
* +-----+ This gap between the `HitRect` and
* `PressRect` allows a touch to move far away
* from the original hit rect, and remain
* highlighted, and eligible for a "Press".
* Customize this via
* `touchableGetPressRectOffset()`.
*
*
*
* ======= State Machine =======
*
* +-------------+ <---+ RESPONDER_RELEASE
* |NOT_RESPONDER|
* +-------------+ <---+ RESPONDER_TERMINATED
* +
* | RESPONDER_GRANT (HitRect)
* v
* +---------------------------+ DELAY +-------------------------+ T + DELAY +------------------------------+
* |RESPONDER_INACTIVE_PRESS_IN|+-------->|RESPONDER_ACTIVE_PRESS_IN| +------------> |RESPONDER_ACTIVE_LONG_PRESS_IN|
* +---------------------------+ +-------------------------+ +------------------------------+
* + ^ + ^ + ^
* |LEAVE_ |ENTER_ |LEAVE_ |ENTER_ |LEAVE_ |ENTER_
* |PRESS_RECT |PRESS_RECT |PRESS_RECT |PRESS_RECT |PRESS_RECT |PRESS_RECT
* | | | | | |
* v + v + v +
* +----------------------------+ DELAY +--------------------------+ +-------------------------------+
* |RESPONDER_INACTIVE_PRESS_OUT|+------->|RESPONDER_ACTIVE_PRESS_OUT| |RESPONDER_ACTIVE_LONG_PRESS_OUT|
* +----------------------------+ +--------------------------+ +-------------------------------+
*
* T + DELAY => LONG_PRESS_DELAY_MS + DELAY
*
* Not drawn are the side effects of each transition. The most important side
* effect is the `touchableHandlePress` abstract method invocation that occurs
* when a responder is released while in either of the "Press" states.
*
* The other important side effects are the highlight abstract method
* invocations (internal callbacks) to be implemented by the mixer.
*
*
* @lends Touchable.prototype
*/
const TouchableMixinImpl = {
componentDidMount: function () {
if (!Platform.isTV) {
return;
}
},
/**
* Clear all timeouts on unmount
*/
/* $FlowFixMe[missing-this-annot] The 'this' type annotation(s) required by
* Flow's LTI update could not be added via codemod */
componentWillUnmount: function () {
this.touchableDelayTimeout && clearTimeout(this.touchableDelayTimeout);
this.longPressDelayTimeout && clearTimeout(this.longPressDelayTimeout);
this.pressOutDelayTimeout && clearTimeout(this.pressOutDelayTimeout);
},
/**
* It's prefer that mixins determine state in this way, having the class
* explicitly mix the state in the one and only `getInitialState` method.
*
* @return {object} State object to be placed inside of
* `this.state.touchable`.
*/
touchableGetInitialState: function (): {
touchable: {
touchState: ?TouchableState,
responderID: ?GestureResponderEvent['currentTarget'],
},
} {
return {
touchable: {touchState: undefined, responderID: null},
};
},
// ==== Hooks to Gesture Responder system ====
/**
* Must return true if embedded in a native platform scroll view.
*/
/* $FlowFixMe[missing-this-annot] The 'this' type annotation(s) required by
* Flow's LTI update could not be added via codemod */
touchableHandleResponderTerminationRequest: function (): any {
return !this.props.rejectResponderTermination;
},
/**
* Must return true to start the process of `Touchable`.
*/
/* $FlowFixMe[missing-this-annot] The 'this' type annotation(s) required by
* Flow's LTI update could not be added via codemod */
touchableHandleStartShouldSetResponder: function (): any {
return !this.props.disabled;
},
/**
* Return true to cancel press on long press.
*/
touchableLongPressCancelsPress: function (): boolean {
return true;
},
/**
* Place as callback for a DOM element's `onResponderGrant` event.
* @param {NativeSyntheticEvent} e Synthetic event from event system.
*
*/
/* $FlowFixMe[missing-this-annot] The 'this' type annotation(s) required by
* Flow's LTI update could not be added via codemod */
touchableHandleResponderGrant: function (e: GestureResponderEvent) {
const dispatchID = e.currentTarget;
// Since e is used in a callback invoked on another event loop
// (as in setTimeout etc), we need to call e.persist() on the
// event to make sure it doesn't get reused in the event object pool.
e.persist();
this.pressOutDelayTimeout && clearTimeout(this.pressOutDelayTimeout);
this.pressOutDelayTimeout = null;
this.state.touchable.touchState = States.NOT_RESPONDER;
this.state.touchable.responderID = dispatchID;
this._receiveSignal(Signals.RESPONDER_GRANT, e);
let delayMS =
this.touchableGetHighlightDelayMS !== undefined
? Math.max(this.touchableGetHighlightDelayMS(), 0)
: HIGHLIGHT_DELAY_MS;
delayMS = isNaN(delayMS) ? HIGHLIGHT_DELAY_MS : delayMS;
if (delayMS !== 0) {
this.touchableDelayTimeout = setTimeout(
this._handleDelay.bind(this, e),
delayMS,
);
} else {
this._handleDelay(e);
}
let longDelayMS =
this.touchableGetLongPressDelayMS !== undefined
? Math.max(this.touchableGetLongPressDelayMS(), 10)
: LONG_PRESS_DELAY_MS;
longDelayMS = isNaN(longDelayMS) ? LONG_PRESS_DELAY_MS : longDelayMS;
this.longPressDelayTimeout = setTimeout(
this._handleLongDelay.bind(this, e),
longDelayMS + delayMS,
);
},
/**
* Place as callback for a DOM element's `onResponderRelease` event.
*/
/* $FlowFixMe[missing-this-annot] The 'this' type annotation(s) required by
* Flow's LTI update could not be added via codemod */
touchableHandleResponderRelease: function (e: GestureResponderEvent) {
this.pressInLocation = null;
this._receiveSignal(Signals.RESPONDER_RELEASE, e);
},
/**
* Place as callback for a DOM element's `onResponderTerminate` event.
*/
/* $FlowFixMe[missing-this-annot] The 'this' type annotation(s) required by
* Flow's LTI update could not be added via codemod */
touchableHandleResponderTerminate: function (e: GestureResponderEvent) {
this.pressInLocation = null;
this._receiveSignal(Signals.RESPONDER_TERMINATED, e);
},
/**
* Place as callback for a DOM element's `onResponderMove` event.
*/
/* $FlowFixMe[missing-this-annot] The 'this' type annotation(s) required by
* Flow's LTI update could not be added via codemod */
touchableHandleResponderMove: function (e: GestureResponderEvent) {
// Measurement may not have returned yet.
if (!this.state.touchable.positionOnActivate) {
return;
}
const positionOnActivate = this.state.touchable.positionOnActivate;
const dimensionsOnActivate = this.state.touchable.dimensionsOnActivate;
const pressRectOffset = this.touchableGetPressRectOffset
? this.touchableGetPressRectOffset()
: {
left: PRESS_EXPAND_PX,
right: PRESS_EXPAND_PX,
top: PRESS_EXPAND_PX,
bottom: PRESS_EXPAND_PX,
};
let pressExpandLeft = pressRectOffset.left;
let pressExpandTop = pressRectOffset.top;
let pressExpandRight = pressRectOffset.right;
let pressExpandBottom = pressRectOffset.bottom;
const hitSlop = this.touchableGetHitSlop
? this.touchableGetHitSlop()
: null;
if (hitSlop) {
pressExpandLeft += hitSlop.left || 0;
pressExpandTop += hitSlop.top || 0;
pressExpandRight += hitSlop.right || 0;
pressExpandBottom += hitSlop.bottom || 0;
}
const touch = extractSingleTouch(e.nativeEvent);
const pageX = touch && touch.pageX;
const pageY = touch && touch.pageY;
if (this.pressInLocation) {
const movedDistance = this._getDistanceBetweenPoints(
pageX,
pageY,
this.pressInLocation.pageX,
this.pressInLocation.pageY,
);
if (movedDistance > LONG_PRESS_ALLOWED_MOVEMENT) {
this._cancelLongPressDelayTimeout();
}
}
const isTouchWithinActive =
pageX > positionOnActivate.left - pressExpandLeft &&
pageY > positionOnActivate.top - pressExpandTop &&
pageX <
positionOnActivate.left +
dimensionsOnActivate.width +
pressExpandRight &&
pageY <
positionOnActivate.top +
dimensionsOnActivate.height +
pressExpandBottom;
if (isTouchWithinActive) {
const prevState = this.state.touchable.touchState;
this._receiveSignal(Signals.ENTER_PRESS_RECT, e);
const curState = this.state.touchable.touchState;
if (
curState === States.RESPONDER_INACTIVE_PRESS_IN &&
prevState !== States.RESPONDER_INACTIVE_PRESS_IN
) {
// fix for t7967420
this._cancelLongPressDelayTimeout();
}
} else {
this._cancelLongPressDelayTimeout();
this._receiveSignal(Signals.LEAVE_PRESS_RECT, e);
}
},
/**
* Invoked when the item receives focus. Mixers might override this to
* visually distinguish the `VisualRect` so that the user knows that it
* currently has the focus. Most platforms only support a single element being
* focused at a time, in which case there may have been a previously focused
* element that was blurred just prior to this. This can be overridden when
* using `Touchable.Mixin.withoutDefaultFocusAndBlur`.
*/
/* $FlowFixMe[missing-this-annot] The 'this' type annotation(s) required by
* Flow's LTI update could not be added via codemod */
touchableHandleFocus: function (e: FocusEvent) {
this.props.onFocus && this.props.onFocus(e);
},
/**
* Invoked when the item loses focus. Mixers might override this to
* visually distinguish the `VisualRect` so that the user knows that it
* no longer has focus. Most platforms only support a single element being
* focused at a time, in which case the focus may have moved to another.
* This can be overridden when using
* `Touchable.Mixin.withoutDefaultFocusAndBlur`.
*/
/* $FlowFixMe[missing-this-annot] The 'this' type annotation(s) required by
* Flow's LTI update could not be added via codemod */
touchableHandleBlur: function (e: BlurEvent) {
this.props.onBlur && this.props.onBlur(e);
},
// ==== Abstract Application Callbacks ====
/**
* Invoked when the item should be highlighted. Mixers should implement this
* to visually distinguish the `VisualRect` so that the user knows that
* releasing a touch will result in a "selection" (analog to click).
*
* @abstract
* touchableHandleActivePressIn: function,
*/
/**
* Invoked when the item is "active" (in that it is still eligible to become
* a "select") but the touch has left the `PressRect`. Usually the mixer will
* want to unhighlight the `VisualRect`. If the user (while pressing) moves
* back into the `PressRect` `touchableHandleActivePressIn` will be invoked
* again and the mixer should probably highlight the `VisualRect` again. This
* event will not fire on an `touchEnd/mouseUp` event, only move events while
* the user is depressing the mouse/touch.
*
* @abstract
* touchableHandleActivePressOut: function
*/
/**
* Invoked when the item is "selected" - meaning the interaction ended by
* letting up while the item was either in the state
* `RESPONDER_ACTIVE_PRESS_IN` or `RESPONDER_INACTIVE_PRESS_IN`.
*
* @abstract
* touchableHandlePress: function
*/
/**
* Invoked when the item is long pressed - meaning the interaction ended by
* letting up while the item was in `RESPONDER_ACTIVE_LONG_PRESS_IN`. If
* `touchableHandleLongPress` is *not* provided, `touchableHandlePress` will
* be called as it normally is. If `touchableHandleLongPress` is provided, by
* default any `touchableHandlePress` callback will not be invoked. To
* override this default behavior, override `touchableLongPressCancelsPress`
* to return false. As a result, `touchableHandlePress` will be called when
* lifting up, even if `touchableHandleLongPress` has also been called.
*
* @abstract
* touchableHandleLongPress: function
*/
/**
* Returns the number of millis to wait before triggering a highlight.
*
* @abstract
* touchableGetHighlightDelayMS: function
*/
/**
* Returns the amount to extend the `HitRect` into the `PressRect`. Positive
* numbers mean the size expands outwards.
*
* @abstract
* touchableGetPressRectOffset: function
*/
// ==== Internal Logic ====
/**
* Measures the `HitRect` node on activation. The Bounding rectangle is with
* respect to viewport - not page, so adding the `pageXOffset/pageYOffset`
* should result in points that are in the same coordinate system as an
* event's `globalX/globalY` data values.
*
* - Consider caching this for the lifetime of the component, or possibly
* being able to share this cache between any `ScrollMap` view.
*
* @sideeffects
* @private
*/
/* $FlowFixMe[missing-this-annot] The 'this' type annotation(s) required by
* Flow's LTI update could not be added via codemod */
_remeasureMetricsOnActivation: function () {
const responderID = this.state.touchable.responderID;
if (responderID == null) {
return;
}
if (typeof responderID === 'number') {
UIManager.measure(responderID, this._handleQueryLayout);
} else {
responderID.measure(this._handleQueryLayout);
}
},
/* $FlowFixMe[missing-this-annot] The 'this' type annotation(s) required by
* Flow's LTI update could not be added via codemod */
_handleQueryLayout: function (
l: number,
t: number,
w: number,
h: number,
globalX: number,
globalY: number,
) {
//don't do anything UIManager failed to measure node
if (!l && !t && !w && !h && !globalX && !globalY) {
return;
}
this.state.touchable.positionOnActivate &&
// $FlowFixMe[prop-missing]
Position.release(this.state.touchable.positionOnActivate);
this.state.touchable.dimensionsOnActivate &&
// $FlowFixMe[prop-missing]
BoundingDimensions.release(this.state.touchable.dimensionsOnActivate);
// $FlowFixMe[prop-missing]
this.state.touchable.positionOnActivate = Position.getPooled(
globalX,
globalY,
);
// $FlowFixMe[prop-missing]
this.state.touchable.dimensionsOnActivate = BoundingDimensions.getPooled(
w,
h,
);
},
/* $FlowFixMe[missing-this-annot] The 'this' type annotation(s) required by
* Flow's LTI update could not be added via codemod */
_handleDelay: function (e: GestureResponderEvent) {
this.touchableDelayTimeout = null;
this._receiveSignal(Signals.DELAY, e);
},
/* $FlowFixMe[missing-this-annot] The 'this' type annotation(s) required by
* Flow's LTI update could not be added via codemod */
_handleLongDelay: function (e: GestureResponderEvent) {
this.longPressDelayTimeout = null;
const curState = this.state.touchable.touchState;
if (
curState === States.RESPONDER_ACTIVE_PRESS_IN ||
curState === States.RESPONDER_ACTIVE_LONG_PRESS_IN
) {
this._receiveSignal(Signals.LONG_PRESS_DETECTED, e);
}
},
/**
* Receives a state machine signal, performs side effects of the transition
* and stores the new state. Validates the transition as well.
*
* @param {Signals} signal State machine signal.
* @throws Error if invalid state transition or unrecognized signal.
* @sideeffects
*/
/* $FlowFixMe[missing-this-annot] The 'this' type annotation(s) required by
* Flow's LTI update could not be added via codemod */
_receiveSignal: function (signal: Signal, e: GestureResponderEvent) {
const responderID = this.state.touchable.responderID;
const curState = this.state.touchable.touchState;
const nextState = Transitions[curState] && Transitions[curState][signal];
if (!responderID && signal === Signals.RESPONDER_RELEASE) {
return;
}
if (!nextState) {
throw new Error(
'Unrecognized signal `' +
signal +
'` or state `' +
curState +
'` for Touchable responder `' +
typeof this.state.touchable.responderID ===
'number'
? this.state.touchable.responderID
: 'host component' + '`',
);
}
if (nextState === States.ERROR) {
throw new Error(
'Touchable cannot transition from `' +
curState +
'` to `' +
signal +
'` for responder `' +
typeof this.state.touchable.responderID ===
'number'
? this.state.touchable.responderID
: '<<host component>>' + '`',
);
}
if (curState !== nextState) {
this._performSideEffectsForTransition(curState, nextState, signal, e);
this.state.touchable.touchState = nextState;
}
},
/* $FlowFixMe[missing-this-annot] The 'this' type annotation(s) required by
* Flow's LTI update could not be added via codemod */
_cancelLongPressDelayTimeout: function () {
this.longPressDelayTimeout && clearTimeout(this.longPressDelayTimeout);
this.longPressDelayTimeout = null;
},
_isHighlight: function (state: TouchableState): boolean {
return (
state === States.RESPONDER_ACTIVE_PRESS_IN ||
state === States.RESPONDER_ACTIVE_LONG_PRESS_IN
);
},
/* $FlowFixMe[missing-this-annot] The 'this' type annotation(s) required by
* Flow's LTI update could not be added via codemod */
_savePressInLocation: function (e: GestureResponderEvent) {
const touch = extractSingleTouch(e.nativeEvent);
const pageX = touch && touch.pageX;
const pageY = touch && touch.pageY;
const locationX = touch && touch.locationX;
const locationY = touch && touch.locationY;
this.pressInLocation = {pageX, pageY, locationX, locationY};
},
_getDistanceBetweenPoints: function (
aX: number,
aY: number,
bX: number,
bY: number,
): number {
const deltaX = aX - bX;
const deltaY = aY - bY;
return Math.sqrt(deltaX * deltaX + deltaY * deltaY);
},
/**
* Will perform a transition between touchable states, and identify any
* highlighting or unhighlighting that must be performed for this particular
* transition.
*
* @param {States} curState Current Touchable state.
* @param {States} nextState Next Touchable state.
* @param {Signal} signal Signal that triggered the transition.
* @param {Event} e Native event.
* @sideeffects
*/
/* $FlowFixMe[missing-this-annot] The 'this' type annotation(s) required by
* Flow's LTI update could not be added via codemod */
_performSideEffectsForTransition: function (
curState: TouchableState,
nextState: TouchableState,
signal: Signal,
e: GestureResponderEvent,
) {
const curIsHighlight = this._isHighlight(curState);
const newIsHighlight = this._isHighlight(nextState);
const isFinalSignal =
signal === Signals.RESPONDER_TERMINATED ||
signal === Signals.RESPONDER_RELEASE;
if (isFinalSignal) {
this._cancelLongPressDelayTimeout();
}
const isInitialTransition =
curState === States.NOT_RESPONDER &&
nextState === States.RESPONDER_INACTIVE_PRESS_IN;
/* $FlowFixMe[invalid-computed-prop] Natural Inference rollout. See
* https://fburl.com/workplace/6291gfvu */
const isActiveTransition = !IsActive[curState] && IsActive[nextState];
if (isInitialTransition || isActiveTransition) {
this._remeasureMetricsOnActivation();
}
/* $FlowFixMe[invalid-computed-prop] Natural Inference rollout. See
* https://fburl.com/workplace/6291gfvu */
if (IsPressingIn[curState] && signal === Signals.LONG_PRESS_DETECTED) {
this.touchableHandleLongPress && this.touchableHandleLongPress(e);
}
if (newIsHighlight && !curIsHighlight) {
this._startHighlight(e);
} else if (!newIsHighlight && curIsHighlight) {
this._endHighlight(e);
}
/* $FlowFixMe[invalid-computed-prop] Natural Inference rollout. See
* https://fburl.com/workplace/6291gfvu */
if (IsPressingIn[curState] && signal === Signals.RESPONDER_RELEASE) {
const hasLongPressHandler = !!this.props.onLongPress;
const pressIsLongButStillCallOnPress =
/* $FlowFixMe[invalid-computed-prop] Natural Inference rollout. See
* https://fburl.com/workplace/6291gfvu */
IsLongPressingIn[curState] && // We *are* long pressing.. // But either has no long handler
(!hasLongPressHandler || !this.touchableLongPressCancelsPress()); // or we're told to ignore it.
const shouldInvokePress =
/* $FlowFixMe[invalid-computed-prop] Natural Inference rollout. See
* https://fburl.com/workplace/6291gfvu */
!IsLongPressingIn[curState] || pressIsLongButStillCallOnPress;
if (shouldInvokePress && this.touchableHandlePress) {
if (!newIsHighlight && !curIsHighlight) {
// we never highlighted because of delay, but we should highlight now
this._startHighlight(e);
this._endHighlight(e);
}
if (Platform.OS === 'android' && !this.props.touchSoundDisabled) {
SoundManager.playTouchSound();
}
this.touchableHandlePress(e);
}
}
this.touchableDelayTimeout && clearTimeout(this.touchableDelayTimeout);
this.touchableDelayTimeout = null;
},
/* $FlowFixMe[missing-this-annot] The 'this' type annotation(s) required by
* Flow's LTI update could not be added via codemod */
_startHighlight: function (e: GestureResponderEvent) {
this._savePressInLocation(e);
this.touchableHandleActivePressIn && this.touchableHandleActivePressIn(e);
},
/* $FlowFixMe[missing-this-annot] The 'this' type annotation(s) required by
* Flow's LTI update could not be added via codemod */
_endHighlight: function (e: GestureResponderEvent) {
if (this.touchableHandleActivePressOut) {
if (
this.touchableGetPressOutDelayMS &&
this.touchableGetPressOutDelayMS()
) {
this.pressOutDelayTimeout = setTimeout(() => {
this.touchableHandleActivePressOut(e);
}, this.touchableGetPressOutDelayMS());
} else {
this.touchableHandleActivePressOut(e);
}
}
},
withoutDefaultFocusAndBlur: ({}: {...}),
};
/**
* Provide an optional version of the mixin where `touchableHandleFocus` and
* `touchableHandleBlur` can be overridden. This allows appropriate defaults to
* be set on TV platforms, without breaking existing implementations of
* `Touchable`.
*/
const {
touchableHandleFocus,
touchableHandleBlur,
...TouchableMixinWithoutDefaultFocusAndBlur
} = TouchableMixinImpl;
TouchableMixinImpl.withoutDefaultFocusAndBlur =
TouchableMixinWithoutDefaultFocusAndBlur;
const TouchableImpl = {
Mixin: TouchableMixinImpl,
/**
* Renders a debugging overlay to visualize touch target with hitSlop (might not work on Android).
*/
renderDebugView: ({
color,
hitSlop,
}: {
color: ColorValue,
hitSlop?: EdgeInsetsProp,
...
}): null | React.Node => {
if (__DEV__) {
return <PressabilityDebugView color={color} hitSlop={hitSlop} />;
}
return null;
},
};
export default TouchableImpl;

View File

@@ -0,0 +1,235 @@
/**
* 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.
*
* @flow strict-local
* @format
*/
import type {ViewStyleProp} from '../../StyleSheet/StyleSheet';
import typeof TouchableWithoutFeedback from './TouchableWithoutFeedback';
import Animated from '../../Animated/Animated';
import Pressability, {
type PressabilityConfig,
} from '../../Pressability/Pressability';
import {PressabilityDebugView} from '../../Pressability/PressabilityDebug';
import Platform from '../../Utilities/Platform';
import * as React from 'react';
type TouchableBounceProps = $ReadOnly<{
...React.ElementConfig<TouchableWithoutFeedback>,
onPressAnimationComplete?: ?() => void,
onPressWithCompletion?: ?(callback: () => void) => void,
releaseBounciness?: ?number,
releaseVelocity?: ?number,
style?: ?ViewStyleProp,
hostRef: React.RefSetter<React.ElementRef<typeof Animated.View>>,
}>;
type TouchableBounceState = $ReadOnly<{
pressability: Pressability,
scale: Animated.Value,
}>;
class TouchableBounce extends React.Component<
TouchableBounceProps,
TouchableBounceState,
> {
state: TouchableBounceState = {
pressability: new Pressability(this._createPressabilityConfig()),
scale: new Animated.Value(1),
};
_createPressabilityConfig(): PressabilityConfig {
return {
android_disableSound: this.props.touchSoundDisabled,
cancelable: !this.props.rejectResponderTermination,
delayLongPress: this.props.delayLongPress,
delayPressIn: this.props.delayPressIn,
delayPressOut: this.props.delayPressOut,
disabled: this.props.disabled,
hitSlop: this.props.hitSlop,
minPressDuration: 0,
onBlur: event => {
if (Platform.isTV) {
this._bounceTo(1, 0.4, 0);
}
if (this.props.onBlur != null) {
this.props.onBlur(event);
}
},
onFocus: event => {
if (Platform.isTV) {
this._bounceTo(0.93, 0.1, 0);
}
if (this.props.onFocus != null) {
this.props.onFocus(event);
}
},
onLongPress: this.props.onLongPress,
onPress: event => {
const {onPressAnimationComplete, onPressWithCompletion} = this.props;
const releaseBounciness = this.props.releaseBounciness ?? 10;
const releaseVelocity = this.props.releaseVelocity ?? 10;
if (onPressWithCompletion != null) {
onPressWithCompletion(() => {
this.state.scale.setValue(0.93);
this._bounceTo(
1,
releaseVelocity,
releaseBounciness,
onPressAnimationComplete,
);
});
return;
}
this._bounceTo(
1,
releaseVelocity,
releaseBounciness,
onPressAnimationComplete,
);
if (this.props.onPress != null) {
this.props.onPress(event);
}
},
onPressIn: event => {
this._bounceTo(0.93, 0.1, 0);
if (this.props.onPressIn != null) {
this.props.onPressIn(event);
}
},
onPressOut: event => {
this._bounceTo(1, 0.4, 0);
if (this.props.onPressOut != null) {
this.props.onPressOut(event);
}
},
pressRectOffset: this.props.pressRetentionOffset,
};
}
_bounceTo(
toValue: number,
velocity: number,
bounciness: number,
callback?: ?() => void,
) {
Animated.spring(this.state.scale, {
bounciness,
toValue,
useNativeDriver: true,
velocity,
}).start(callback);
}
render(): React.Node {
// BACKWARD-COMPATIBILITY: Focus and blur events were never supported before
// adopting `Pressability`, so preserve that behavior.
const {onBlur, onFocus, ...eventHandlersWithoutBlurAndFocus} =
this.state.pressability.getEventHandlers();
const accessibilityLiveRegion =
this.props['aria-live'] === 'off'
? 'none'
: (this.props['aria-live'] ?? this.props.accessibilityLiveRegion);
const _accessibilityState = {
busy: this.props['aria-busy'] ?? this.props.accessibilityState?.busy,
checked:
this.props['aria-checked'] ?? this.props.accessibilityState?.checked,
disabled:
this.props['aria-disabled'] ?? this.props.accessibilityState?.disabled,
expanded:
this.props['aria-expanded'] ?? this.props.accessibilityState?.expanded,
selected:
this.props['aria-selected'] ?? this.props.accessibilityState?.selected,
};
const accessibilityValue = {
max: this.props['aria-valuemax'] ?? this.props.accessibilityValue?.max,
min: this.props['aria-valuemin'] ?? this.props.accessibilityValue?.min,
now: this.props['aria-valuenow'] ?? this.props.accessibilityValue?.now,
text: this.props['aria-valuetext'] ?? this.props.accessibilityValue?.text,
};
const accessibilityLabel =
this.props['aria-label'] ?? this.props.accessibilityLabel;
return (
<Animated.View
style={[{transform: [{scale: this.state.scale}]}, this.props.style]}
accessible={this.props.accessible !== false}
accessibilityLabel={accessibilityLabel}
accessibilityHint={this.props.accessibilityHint}
accessibilityLanguage={this.props.accessibilityLanguage}
accessibilityRole={this.props.accessibilityRole}
accessibilityState={_accessibilityState}
accessibilityActions={this.props.accessibilityActions}
onAccessibilityAction={this.props.onAccessibilityAction}
accessibilityValue={accessibilityValue}
accessibilityLiveRegion={accessibilityLiveRegion}
importantForAccessibility={
this.props['aria-hidden'] === true
? 'no-hide-descendants'
: // $FlowFixMe[incompatible-type] - AnimatedProps types were made more strict and need refining at this call site
this.props.importantForAccessibility
}
accessibilityViewIsModal={
this.props['aria-modal'] ?? this.props.accessibilityViewIsModal
}
accessibilityElementsHidden={
this.props['aria-hidden'] ?? this.props.accessibilityElementsHidden
}
nativeID={this.props.id ?? this.props.nativeID}
testID={this.props.testID}
hitSlop={this.props.hitSlop}
focusable={
this.props.focusable !== false &&
this.props.onPress !== undefined &&
!this.props.disabled
}
// $FlowFixMe[prop-missing]
ref={this.props.hostRef}
{...eventHandlersWithoutBlurAndFocus}>
{this.props.children}
{__DEV__ ? (
<PressabilityDebugView color="orange" hitSlop={this.props.hitSlop} />
) : null}
</Animated.View>
);
}
componentDidUpdate(
prevProps: TouchableBounceProps,
prevState: TouchableBounceState,
) {
this.state.pressability.configure(this._createPressabilityConfig());
}
componentDidMount(): mixed {
this.state.pressability.configure(this._createPressabilityConfig());
}
componentWillUnmount(): void {
this.state.pressability.reset();
this.state.scale.resetAnimation();
}
}
export default (function TouchableBounceWrapper({
ref: hostRef,
...props
}: {
ref: React.RefSetter<mixed>,
...$ReadOnly<Omit<TouchableBounceProps, 'hostRef'>>,
}) {
return <TouchableBounce {...props} hostRef={hostRef} />;
} as component(
ref?: React.RefSetter<mixed>,
...props: $ReadOnly<Omit<TouchableBounceProps, 'hostRef'>>
));

View File

@@ -0,0 +1,62 @@
/**
* 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
*/
import type * as React from 'react';
import {ColorValue, StyleProp} from '../../StyleSheet/StyleSheet';
import {ViewStyle} from '../../StyleSheet/StyleSheetTypes';
import {View} from '../../Components/View/View';
import {TouchableWithoutFeedbackProps} from './TouchableWithoutFeedback';
/**
* @see https://reactnative.dev/docs/touchablehighlight#props
*/
export interface TouchableHighlightProps extends TouchableWithoutFeedbackProps {
/**
* Determines what the opacity of the wrapped view should be when touch is active.
*/
activeOpacity?: number | undefined;
/**
*
* Called immediately after the underlay is hidden
*/
onHideUnderlay?: (() => void) | undefined;
/**
* Called immediately after the underlay is shown
*/
onShowUnderlay?: (() => void) | undefined;
/**
* @see https://reactnative.dev/docs/view#style
*/
style?: StyleProp<ViewStyle> | undefined;
/**
* The color of the underlay that will show through when the touch is active.
*/
underlayColor?: ColorValue | undefined;
}
/**
* A wrapper for making views respond properly to touches.
* On press down, the opacity of the wrapped view is decreased,
* which allows the underlay color to show through, darkening or tinting the view.
* The underlay comes from adding a view to the view hierarchy,
* which can sometimes cause unwanted visual artifacts if not used correctly,
* for example if the backgroundColor of the wrapped view isn't explicitly set to an opaque color.
*
* NOTE: TouchableHighlight supports only one child
* If you wish to have several child components, wrap them in a View.
*
* @see https://reactnative.dev/docs/touchablehighlight
*/
export const TouchableHighlight: React.ForwardRefExoticComponent<
React.PropsWithoutRef<TouchableHighlightProps> & React.RefAttributes<View>
>;

View File

@@ -0,0 +1,426 @@
/**
* 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.
*
* @flow strict-local
* @format
*/
import type {ColorValue} from '../../StyleSheet/StyleSheet';
import type {AccessibilityState} from '../View/ViewAccessibility';
import type {TouchableWithoutFeedbackProps} from './TouchableWithoutFeedback';
import View from '../../Components/View/View';
import Pressability, {
type PressabilityConfig,
} from '../../Pressability/Pressability';
import {PressabilityDebugView} from '../../Pressability/PressabilityDebug';
import StyleSheet, {type ViewStyleProp} from '../../StyleSheet/StyleSheet';
import Platform from '../../Utilities/Platform';
import * as React from 'react';
import {cloneElement} from 'react';
type AndroidProps = $ReadOnly<{
nextFocusDown?: ?number,
nextFocusForward?: ?number,
nextFocusLeft?: ?number,
nextFocusRight?: ?number,
nextFocusUp?: ?number,
}>;
type IOSProps = $ReadOnly<{
/**
* @deprecated Use `focusable` instead
*/
hasTVPreferredFocus?: ?boolean,
}>;
type TouchableHighlightBaseProps = $ReadOnly<{
/**
* Determines what the opacity of the wrapped view should be when touch is active.
*/
activeOpacity?: ?number,
/**
* The color of the underlay that will show through when the touch is active.
*/
underlayColor?: ?ColorValue,
/**
* @see https://reactnative.dev/docs/view#style
*/
style?: ?ViewStyleProp,
/**
* Called immediately after the underlay is shown
*/
onShowUnderlay?: ?() => void,
/**
* Called immediately after the underlay is hidden
*/
onHideUnderlay?: ?() => void,
testOnly_pressed?: ?boolean,
hostRef?: React.RefSetter<React.ElementRef<typeof View>>,
}>;
export type TouchableHighlightProps = $ReadOnly<{
...TouchableWithoutFeedbackProps,
...AndroidProps,
...IOSProps,
...TouchableHighlightBaseProps,
}>;
type ExtraStyles = $ReadOnly<{
child: ViewStyleProp,
underlay: ViewStyleProp,
}>;
type TouchableHighlightState = $ReadOnly<{
pressability: Pressability,
extraStyles: ?ExtraStyles,
}>;
/**
* A wrapper for making views respond properly to touches.
* On press down, the opacity of the wrapped view is decreased, which allows
* the underlay color to show through, darkening or tinting the view.
*
* The underlay comes from wrapping the child in a new View, which can affect
* layout, and sometimes cause unwanted visual artifacts if not used correctly,
* for example if the backgroundColor of the wrapped view isn't explicitly set
* to an opaque color.
*
* TouchableHighlight must have one child (not zero or more than one).
* If you wish to have several child components, wrap them in a View.
*
* Example:
*
* ```
* renderButton: function() {
* return (
* <TouchableHighlight onPress={this._onPressButton}>
* <Image
* style={styles.button}
* source={require('./myButton.png')}
* />
* </TouchableHighlight>
* );
* },
* ```
*
*
* ### Example
*
* ```ReactNativeWebPlayer
* import React, { Component } from 'react'
* import {
* AppRegistry,
* StyleSheet,
* TouchableHighlight,
* Text,
* View,
* } from 'react-native'
*
* class App extends Component {
* constructor(props) {
* super(props)
* this.state = { count: 0 }
* }
*
* onPress = () => {
* this.setState({
* count: this.state.count+1
* })
* }
*
* render() {
* return (
* <View style={styles.container}>
* <TouchableHighlight
* style={styles.button}
* onPress={this.onPress}
* >
* <Text> Touch Here </Text>
* </TouchableHighlight>
* <View style={[styles.countContainer]}>
* <Text style={[styles.countText]}>
* { this.state.count !== 0 ? this.state.count: null}
* </Text>
* </View>
* </View>
* )
* }
* }
*
* const styles = StyleSheet.create({
* container: {
* flex: 1,
* justifyContent: 'center',
* paddingHorizontal: 10
* },
* button: {
* alignItems: 'center',
* backgroundColor: '#DDDDDD',
* padding: 10
* },
* countContainer: {
* alignItems: 'center',
* padding: 10
* },
* countText: {
* color: '#FF00FF'
* }
* })
*
* AppRegistry.registerComponent('App', () => App)
* ```
*
*/
class TouchableHighlightImpl extends React.Component<
TouchableHighlightProps,
TouchableHighlightState,
> {
_hideTimeout: ?TimeoutID;
_isMounted: boolean = false;
state: TouchableHighlightState = {
pressability: new Pressability(this._createPressabilityConfig()),
extraStyles:
this.props.testOnly_pressed === true ? this._createExtraStyles() : null,
};
_createPressabilityConfig(): PressabilityConfig {
return {
cancelable: !this.props.rejectResponderTermination,
disabled:
this.props.disabled != null
? this.props.disabled
: this.props.accessibilityState?.disabled,
hitSlop: this.props.hitSlop,
delayLongPress: this.props.delayLongPress,
delayPressIn: this.props.delayPressIn,
delayPressOut: this.props.delayPressOut,
minPressDuration: 0,
pressRectOffset: this.props.pressRetentionOffset,
android_disableSound: this.props.touchSoundDisabled,
onBlur: event => {
if (Platform.isTV) {
this._hideUnderlay();
}
if (this.props.onBlur != null) {
this.props.onBlur(event);
}
},
onFocus: event => {
if (Platform.isTV) {
this._showUnderlay();
}
if (this.props.onFocus != null) {
this.props.onFocus(event);
}
},
onLongPress: this.props.onLongPress,
onPress: event => {
if (this._hideTimeout != null) {
clearTimeout(this._hideTimeout);
}
if (!Platform.isTV) {
this._showUnderlay();
this._hideTimeout = setTimeout(() => {
this._hideUnderlay();
}, this.props.delayPressOut ?? 0);
}
if (this.props.onPress != null) {
this.props.onPress(event);
}
},
onPressIn: event => {
if (this._hideTimeout != null) {
clearTimeout(this._hideTimeout);
this._hideTimeout = null;
}
this._showUnderlay();
if (this.props.onPressIn != null) {
this.props.onPressIn(event);
}
},
onPressOut: event => {
if (this._hideTimeout == null) {
this._hideUnderlay();
}
if (this.props.onPressOut != null) {
this.props.onPressOut(event);
}
},
};
}
_createExtraStyles(): ExtraStyles {
return {
child: {opacity: this.props.activeOpacity ?? 0.85},
underlay: {
backgroundColor:
this.props.underlayColor === undefined
? 'black'
: this.props.underlayColor,
},
};
}
_showUnderlay(): void {
if (!this._isMounted || !this._hasPressHandler()) {
return;
}
this.setState({extraStyles: this._createExtraStyles()});
if (this.props.onShowUnderlay != null) {
this.props.onShowUnderlay();
}
}
_hideUnderlay(): void {
if (this._hideTimeout != null) {
clearTimeout(this._hideTimeout);
this._hideTimeout = null;
}
if (this.props.testOnly_pressed === true) {
return;
}
if (this._hasPressHandler()) {
this.setState({extraStyles: null});
if (this.props.onHideUnderlay != null) {
this.props.onHideUnderlay();
}
}
}
_hasPressHandler(): boolean {
return (
this.props.onPress != null ||
this.props.onPressIn != null ||
this.props.onPressOut != null ||
this.props.onLongPress != null
);
}
render(): React.Node {
const child = React.Children.only<$FlowFixMe>(this.props.children);
// BACKWARD-COMPATIBILITY: Focus and blur events were never supported before
// adopting `Pressability`, so preserve that behavior.
const {onBlur, onFocus, ...eventHandlersWithoutBlurAndFocus} =
this.state.pressability.getEventHandlers();
const accessibilityState: ?AccessibilityState =
this.props.disabled != null
? {
...this.props.accessibilityState,
disabled: this.props.disabled,
}
: this.props.accessibilityState;
const accessibilityValue = {
max: this.props['aria-valuemax'] ?? this.props.accessibilityValue?.max,
min: this.props['aria-valuemin'] ?? this.props.accessibilityValue?.min,
now: this.props['aria-valuenow'] ?? this.props.accessibilityValue?.now,
text: this.props['aria-valuetext'] ?? this.props.accessibilityValue?.text,
};
const accessibilityLiveRegion =
this.props['aria-live'] === 'off'
? 'none'
: (this.props['aria-live'] ?? this.props.accessibilityLiveRegion);
const accessibilityLabel =
this.props['aria-label'] ?? this.props.accessibilityLabel;
return (
<View
accessible={this.props.accessible !== false}
accessibilityLabel={accessibilityLabel}
accessibilityHint={this.props.accessibilityHint}
accessibilityLanguage={this.props.accessibilityLanguage}
accessibilityRole={this.props.accessibilityRole}
accessibilityState={accessibilityState}
accessibilityValue={accessibilityValue}
accessibilityActions={this.props.accessibilityActions}
onAccessibilityAction={this.props.onAccessibilityAction}
importantForAccessibility={
this.props['aria-hidden'] === true
? 'no-hide-descendants'
: this.props.importantForAccessibility
}
accessibilityViewIsModal={
this.props['aria-modal'] ?? this.props.accessibilityViewIsModal
}
accessibilityLiveRegion={accessibilityLiveRegion}
accessibilityElementsHidden={
this.props['aria-hidden'] ?? this.props.accessibilityElementsHidden
}
style={StyleSheet.compose(
this.props.style,
this.state.extraStyles?.underlay,
)}
onLayout={this.props.onLayout}
hitSlop={this.props.hitSlop}
hasTVPreferredFocus={this.props.hasTVPreferredFocus}
nextFocusDown={this.props.nextFocusDown}
nextFocusForward={this.props.nextFocusForward}
nextFocusLeft={this.props.nextFocusLeft}
nextFocusRight={this.props.nextFocusRight}
nextFocusUp={this.props.nextFocusUp}
focusable={
this.props.focusable !== false &&
this.props.onPress !== undefined &&
!this.props.disabled
}
nativeID={this.props.id ?? this.props.nativeID}
testID={this.props.testID}
ref={this.props.hostRef}
{...eventHandlersWithoutBlurAndFocus}>
{cloneElement(child, {
style: StyleSheet.compose(
child.props.style,
this.state.extraStyles?.child,
),
})}
{__DEV__ ? (
<PressabilityDebugView color="green" hitSlop={this.props.hitSlop} />
) : null}
</View>
);
}
componentDidMount(): void {
this._isMounted = true;
this.state.pressability.configure(this._createPressabilityConfig());
}
componentDidUpdate(
prevProps: TouchableHighlightProps,
prevState: TouchableHighlightState,
) {
this.state.pressability.configure(this._createPressabilityConfig());
}
componentWillUnmount(): void {
this._isMounted = false;
if (this._hideTimeout != null) {
clearTimeout(this._hideTimeout);
}
this.state.pressability.reset();
}
}
const TouchableHighlight: component(
ref?: React.RefSetter<React.ElementRef<typeof View>>,
...props: $ReadOnly<Omit<TouchableHighlightProps, 'hostRef'>>
) = ({
ref: hostRef,
...props
}: {
ref?: React.RefSetter<React.ElementRef<typeof View>>,
...$ReadOnly<Omit<TouchableHighlightProps, 'hostRef'>>,
}) => <TouchableHighlightImpl {...props} hostRef={hostRef} />;
TouchableHighlight.displayName = 'TouchableHighlight';
export default TouchableHighlight;

View File

@@ -0,0 +1,114 @@
/**
* 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
*/
import type * as React from 'react';
import {Constructor} from '../../../types/private/Utilities';
import {ColorValue} from '../../StyleSheet/StyleSheet';
import {TouchableMixin} from './Touchable';
import type {TVProps} from './TouchableOpacity';
import {TouchableWithoutFeedbackProps} from './TouchableWithoutFeedback';
interface BaseBackgroundPropType {
type: string;
rippleRadius?: number | null | undefined;
}
interface RippleBackgroundPropType extends BaseBackgroundPropType {
type: 'RippleAndroid';
borderless: boolean;
color?: number | null | undefined;
}
interface ThemeAttributeBackgroundPropType extends BaseBackgroundPropType {
type: 'ThemeAttrAndroid';
attribute: string;
}
type BackgroundPropType =
| RippleBackgroundPropType
| ThemeAttributeBackgroundPropType;
/**
* @see https://reactnative.dev/docs/touchablenativefeedback#props
*/
export interface TouchableNativeFeedbackProps
extends TouchableWithoutFeedbackProps,
TVProps {
/**
* Determines the type of background drawable that's going to be used to display feedback.
* It takes an object with type property and extra data depending on the type.
* It's recommended to use one of the following static methods to generate that dictionary:
* 1) TouchableNativeFeedback.SelectableBackground() - will create object that represents android theme's
* default background for selectable elements (?android:attr/selectableItemBackground)
* 2) TouchableNativeFeedback.SelectableBackgroundBorderless() - will create object that represent android
* theme's default background for borderless selectable elements
* (?android:attr/selectableItemBackgroundBorderless). Available on android API level 21+
* 3) TouchableNativeFeedback.Ripple(color, borderless) - will create object that represents ripple drawable
* with specified color (as a string). If property borderless evaluates to true the ripple will render
* outside of the view bounds (see native actionbar buttons as an example of that behavior). This background
* type is available on Android API level 21+
*/
background?: BackgroundPropType | undefined;
useForeground?: boolean | undefined;
}
/**
* A wrapper for making views respond properly to touches (Android only).
* On Android this component uses native state drawable to display touch feedback.
* At the moment it only supports having a single View instance as a child node,
* as it's implemented by replacing that View with another instance of RCTView node with some additional properties set.
*
* Background drawable of native feedback touchable can be customized with background property.
*
* @see https://reactnative.dev/docs/touchablenativefeedback#content
*/
declare class TouchableNativeFeedbackComponent extends React.Component<TouchableNativeFeedbackProps> {}
declare const TouchableNativeFeedbackBase: Constructor<TouchableMixin> &
typeof TouchableNativeFeedbackComponent;
export class TouchableNativeFeedback extends TouchableNativeFeedbackBase {
/**
* Creates an object that represents android theme's default background for
* selectable elements (?android:attr/selectableItemBackground).
*
* @param rippleRadius The radius of ripple effect
*/
static SelectableBackground(
rippleRadius?: number | null,
): ThemeAttributeBackgroundPropType;
/**
* Creates an object that represent android theme's default background for borderless
* selectable elements (?android:attr/selectableItemBackgroundBorderless).
* Available on android API level 21+.
*
* @param rippleRadius The radius of ripple effect
*/
static SelectableBackgroundBorderless(
rippleRadius?: number | null,
): ThemeAttributeBackgroundPropType;
/**
* Creates an object that represents ripple drawable with specified color (as a
* string). If property `borderless` evaluates to true the ripple will
* render outside of the view bounds (see native actionbar buttons as an
* example of that behavior). This background type is available on Android
* API level 21+.
*
* @param color The ripple color
* @param borderless If the ripple can render outside it's bounds
* @param rippleRadius The radius of ripple effect
*/
static Ripple(
color: ColorValue,
borderless: boolean,
rippleRadius?: number | null,
): RippleBackgroundPropType;
static canUseNativeForeground(): boolean;
}

View File

@@ -0,0 +1,416 @@
/**
* 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.
*
* @flow strict-local
* @format
*/
import type {GestureResponderEvent} from '../../Types/CoreEventTypes';
import type {TouchableWithoutFeedbackProps} from './TouchableWithoutFeedback';
import View from '../../Components/View/View';
import Pressability, {
type PressabilityConfig,
} from '../../Pressability/Pressability';
import {PressabilityDebugView} from '../../Pressability/PressabilityDebug';
import {findHostInstance_DEPRECATED} from '../../ReactNative/RendererProxy';
import processColor from '../../StyleSheet/processColor';
import Platform from '../../Utilities/Platform';
import {Commands} from '../View/ViewNativeComponent';
import invariant from 'invariant';
import * as React from 'react';
import {cloneElement} from 'react';
type TouchableNativeFeedbackTVProps = {
/**
* *(Apple TV only)* TV preferred focus (see documentation for the View component).
*
* @platform ios
* @deprecated Use `focusable` instead
*/
hasTVPreferredFocus?: ?boolean,
/**
* Designates the next view to receive focus when the user navigates down. See the Android documentation.
*
* @platform android
*/
nextFocusDown?: ?number,
/**
* Designates the next view to receive focus when the user navigates forward. See the Android documentation.
*
* @platform android
*/
nextFocusForward?: ?number,
/**
* Designates the next view to receive focus when the user navigates left. See the Android documentation.
*
* @platform android
*/
nextFocusLeft?: ?number,
/**
* Designates the next view to receive focus when the user navigates right. See the Android documentation.
*
* @platform android
*/
nextFocusRight?: ?number,
/**
* Designates the next view to receive focus when the user navigates up. See the Android documentation.
*
* @platform android
*/
nextFocusUp?: ?number,
};
export type TouchableNativeFeedbackProps = $ReadOnly<{
...TouchableWithoutFeedbackProps,
...TouchableNativeFeedbackTVProps,
/**
* Determines the type of background drawable that's going to be used to display feedback.
* It takes an object with type property and extra data depending on the type.
* It's recommended to use one of the following static methods to generate that dictionary:
* 1) TouchableNativeFeedback.SelectableBackground() - will create object that represents android theme's
* default background for selectable elements (?android:attr/selectableItemBackground)
* 2) TouchableNativeFeedback.SelectableBackgroundBorderless() - will create object that represent android
* theme's default background for borderless selectable elements
* (?android:attr/selectableItemBackgroundBorderless). Available on android API level 21+
* 3) TouchableNativeFeedback.Ripple(color, borderless) - will create object that represents ripple drawable
* with specified color (as a string). If property borderless evaluates to true the ripple will render
* outside of the view bounds (see native actionbar buttons as an example of that behavior). This background
* type is available on Android API level 21+
*/
background?: ?(
| $ReadOnly<{
type: 'ThemeAttrAndroid',
attribute:
| 'selectableItemBackground'
| 'selectableItemBackgroundBorderless',
rippleRadius: ?number,
}>
| $ReadOnly<{
type: 'RippleAndroid',
color: ?number,
borderless: boolean,
rippleRadius: ?number,
}>
),
/**
* Set to true to add the ripple effect to the foreground of the view, instead
* of the background. This is useful if one of your child views has a
* background of its own, or you're e.g. displaying images, and you don't want
* the ripple to be covered by them.
*
* Check TouchableNativeFeedback.canUseNativeForeground() first, as this is
* only available on Android 6.0 and above. If you try to use this on older
* versions, this will fallback to background.
*/
useForeground?: ?boolean,
}>;
type TouchableNativeFeedbackState = $ReadOnly<{
pressability: Pressability,
}>;
/**
* A wrapper for making views respond properly to touches (Android only).
* On Android this component uses native state drawable to display touch feedback.
* At the moment it only supports having a single View instance as a child node,
* as it's implemented by replacing that View with another instance of RCTView node with some additional properties set.
*
* Background drawable of native feedback touchable can be customized with background property.
*
* @see https://reactnative.dev/docs/touchablenativefeedback#content
*/
class TouchableNativeFeedback extends React.Component<
TouchableNativeFeedbackProps,
TouchableNativeFeedbackState,
> {
/**
* Creates an object that represents android theme's default background for
* selectable elements (?android:attr/selectableItemBackground).
*
* @param rippleRadius The radius of ripple effect
*/
static SelectableBackground: (rippleRadius?: ?number) => $ReadOnly<{
attribute: 'selectableItemBackground',
type: 'ThemeAttrAndroid',
rippleRadius: ?number,
}> = (rippleRadius?: ?number) => ({
type: 'ThemeAttrAndroid',
attribute: 'selectableItemBackground',
rippleRadius,
});
/**
* Creates an object that represent android theme's default background for borderless
* selectable elements (?android:attr/selectableItemBackgroundBorderless).
* Available on android API level 21+.
*
* @param rippleRadius The radius of ripple effect
*/
static SelectableBackgroundBorderless: (rippleRadius?: ?number) => $ReadOnly<{
attribute: 'selectableItemBackgroundBorderless',
type: 'ThemeAttrAndroid',
rippleRadius: ?number,
}> = (rippleRadius?: ?number) => ({
type: 'ThemeAttrAndroid',
attribute: 'selectableItemBackgroundBorderless',
rippleRadius,
});
/**
* Creates an object that represents ripple drawable with specified color (as a
* string). If property `borderless` evaluates to true the ripple will
* render outside of the view bounds (see native actionbar buttons as an
* example of that behavior). This background type is available on Android
* API level 21+.
*
* @param color The ripple color
* @param borderless If the ripple can render outside it's bounds
* @param rippleRadius The radius of ripple effect
*/
static Ripple: (
color: string,
borderless: boolean,
rippleRadius?: ?number,
) => $ReadOnly<{
borderless: boolean,
color: ?number,
rippleRadius: ?number,
type: 'RippleAndroid',
}> = (color: string, borderless: boolean, rippleRadius?: ?number) => {
const processedColor = processColor(color);
invariant(
processedColor == null || typeof processedColor === 'number',
'Unexpected color given for Ripple color',
);
return {
type: 'RippleAndroid',
// $FlowFixMe[incompatible-type]
color: processedColor,
borderless,
rippleRadius,
};
};
/**
* Whether `useForeground` is supported.
*/
static canUseNativeForeground: () => boolean = () =>
Platform.OS === 'android';
state: TouchableNativeFeedbackState = {
pressability: new Pressability(this._createPressabilityConfig()),
};
_createPressabilityConfig(): PressabilityConfig {
const accessibilityStateDisabled =
this.props['aria-disabled'] ?? this.props.accessibilityState?.disabled;
return {
cancelable: !this.props.rejectResponderTermination,
disabled:
this.props.disabled != null
? this.props.disabled
: accessibilityStateDisabled,
hitSlop: this.props.hitSlop,
delayLongPress: this.props.delayLongPress,
delayPressIn: this.props.delayPressIn,
delayPressOut: this.props.delayPressOut,
minPressDuration: 0,
pressRectOffset: this.props.pressRetentionOffset,
android_disableSound: this.props.touchSoundDisabled,
onLongPress: this.props.onLongPress,
onPress: this.props.onPress,
onPressIn: event => {
if (Platform.OS === 'android') {
this._dispatchHotspotUpdate(event);
this._dispatchPressedStateChange(true);
}
if (this.props.onPressIn != null) {
this.props.onPressIn(event);
}
},
onPressMove: event => {
if (Platform.OS === 'android') {
this._dispatchHotspotUpdate(event);
}
},
onPressOut: event => {
if (Platform.OS === 'android') {
this._dispatchPressedStateChange(false);
}
if (this.props.onPressOut != null) {
this.props.onPressOut(event);
}
},
};
}
_dispatchPressedStateChange(pressed: boolean): void {
if (Platform.OS === 'android') {
const hostComponentRef = findHostInstance_DEPRECATED<$FlowFixMe>(this);
if (hostComponentRef == null) {
console.warn(
'Touchable: Unable to find HostComponent instance. ' +
'Has your Touchable component been unmounted?',
);
} else {
Commands.setPressed(hostComponentRef, pressed);
}
}
}
_dispatchHotspotUpdate(event: GestureResponderEvent): void {
if (Platform.OS === 'android') {
const {locationX, locationY} = event.nativeEvent;
const hostComponentRef = findHostInstance_DEPRECATED<$FlowFixMe>(this);
if (hostComponentRef == null) {
console.warn(
'Touchable: Unable to find HostComponent instance. ' +
'Has your Touchable component been unmounted?',
);
} else {
Commands.hotspotUpdate(
hostComponentRef,
locationX ?? 0,
locationY ?? 0,
);
}
}
}
render(): React.Node {
const element = React.Children.only<$FlowFixMe>(this.props.children);
const children: Array<React.Node> = [element.props.children];
if (__DEV__) {
if (element.type === View) {
children.push(
<PressabilityDebugView color="brown" hitSlop={this.props.hitSlop} />,
);
}
}
// BACKWARD-COMPATIBILITY: Focus and blur events were never supported before
// adopting `Pressability`, so preserve that behavior.
const {onBlur, onFocus, ...eventHandlersWithoutBlurAndFocus} =
this.state.pressability.getEventHandlers();
let _accessibilityState = {
busy: this.props['aria-busy'] ?? this.props.accessibilityState?.busy,
checked:
this.props['aria-checked'] ?? this.props.accessibilityState?.checked,
disabled:
this.props['aria-disabled'] ?? this.props.accessibilityState?.disabled,
expanded:
this.props['aria-expanded'] ?? this.props.accessibilityState?.expanded,
selected:
this.props['aria-selected'] ?? this.props.accessibilityState?.selected,
};
_accessibilityState =
this.props.disabled != null
? {
..._accessibilityState,
disabled: this.props.disabled,
}
: _accessibilityState;
const accessibilityValue = {
max: this.props['aria-valuemax'] ?? this.props.accessibilityValue?.max,
min: this.props['aria-valuemin'] ?? this.props.accessibilityValue?.min,
now: this.props['aria-valuenow'] ?? this.props.accessibilityValue?.now,
text: this.props['aria-valuetext'] ?? this.props.accessibilityValue?.text,
};
const accessibilityLiveRegion =
this.props['aria-live'] === 'off'
? 'none'
: (this.props['aria-live'] ?? this.props.accessibilityLiveRegion);
const accessibilityLabel =
this.props['aria-label'] ?? this.props.accessibilityLabel;
return cloneElement(
element,
{
...eventHandlersWithoutBlurAndFocus,
...getBackgroundProp(
this.props.background === undefined
? TouchableNativeFeedback.SelectableBackground()
: this.props.background,
this.props.useForeground === true,
),
accessible: this.props.accessible !== false,
accessibilityHint: this.props.accessibilityHint,
accessibilityLanguage: this.props.accessibilityLanguage,
accessibilityLabel: accessibilityLabel,
accessibilityRole: this.props.accessibilityRole,
accessibilityState: _accessibilityState,
accessibilityActions: this.props.accessibilityActions,
onAccessibilityAction: this.props.onAccessibilityAction,
accessibilityValue: accessibilityValue,
importantForAccessibility:
this.props['aria-hidden'] === true
? 'no-hide-descendants'
: this.props.importantForAccessibility,
accessibilityViewIsModal:
this.props['aria-modal'] ?? this.props.accessibilityViewIsModal,
accessibilityLiveRegion: accessibilityLiveRegion,
accessibilityElementsHidden:
this.props['aria-hidden'] ?? this.props.accessibilityElementsHidden,
hasTVPreferredFocus: this.props.hasTVPreferredFocus,
hitSlop: this.props.hitSlop,
focusable:
this.props.focusable !== false &&
this.props.onPress !== undefined &&
!this.props.disabled,
nativeID: this.props.id ?? this.props.nativeID,
nextFocusDown: this.props.nextFocusDown,
nextFocusForward: this.props.nextFocusForward,
nextFocusLeft: this.props.nextFocusLeft,
nextFocusRight: this.props.nextFocusRight,
nextFocusUp: this.props.nextFocusUp,
onLayout: this.props.onLayout,
testID: this.props.testID,
},
...children,
);
}
componentDidUpdate(
prevProps: TouchableNativeFeedbackProps,
prevState: TouchableNativeFeedbackState,
) {
this.state.pressability.configure(this._createPressabilityConfig());
}
componentDidMount(): mixed {
this.state.pressability.configure(this._createPressabilityConfig());
}
componentWillUnmount(): void {
this.state.pressability.reset();
}
}
const getBackgroundProp =
Platform.OS === 'android'
? /* $FlowFixMe[missing-local-annot] The type annotation(s) required by
* Flow's LTI update could not be added via codemod */
(background, useForeground: boolean) =>
useForeground && TouchableNativeFeedback.canUseNativeForeground()
? {nativeForegroundAndroid: background}
: {nativeBackgroundAndroid: background}
: /* $FlowFixMe[missing-local-annot] The type annotation(s) required by
* Flow's LTI update could not be added via codemod */
(background, useForeground: boolean) => null;
TouchableNativeFeedback.displayName = 'TouchableNativeFeedback';
export default TouchableNativeFeedback;

View File

@@ -0,0 +1,82 @@
/**
* 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
*/
import type * as React from 'react';
import {View} from '../../Components/View/View';
import {TouchableWithoutFeedbackProps} from './TouchableWithoutFeedback';
export interface TVProps {
/**
* *(Apple TV only)* TV preferred focus (see documentation for the View component).
*
* @platform ios
* @deprecated Use `focusable` instead
*/
hasTVPreferredFocus?: boolean | undefined;
/**
* Designates the next view to receive focus when the user navigates down. See the Android documentation.
*
* @platform android
*/
nextFocusDown?: number | undefined;
/**
* Designates the next view to receive focus when the user navigates forward. See the Android documentation.
*
* @platform android
*/
nextFocusForward?: number | undefined;
/**
* Designates the next view to receive focus when the user navigates left. See the Android documentation.
*
* @platform android
*/
nextFocusLeft?: number | undefined;
/**
* Designates the next view to receive focus when the user navigates right. See the Android documentation.
*
* @platform android
*/
nextFocusRight?: number | undefined;
/**
* Designates the next view to receive focus when the user navigates up. See the Android documentation.
*
* @platform android
*/
nextFocusUp?: number | undefined;
}
/**
* @see https://reactnative.dev/docs/touchableopacity#props
*/
export interface TouchableOpacityProps
extends TouchableWithoutFeedbackProps,
TVProps {
/**
* Determines what the opacity of the wrapped view should be when touch is active.
* Defaults to 0.2
*/
activeOpacity?: number | undefined;
}
/**
* A wrapper for making views respond properly to touches.
* On press down, the opacity of the wrapped view is decreased, dimming it.
* This is done without actually changing the view hierarchy,
* and in general is easy to add to an app without weird side-effects.
*
* @see https://reactnative.dev/docs/touchableopacity
*/
export const TouchableOpacity: React.ForwardRefExoticComponent<
React.PropsWithoutRef<TouchableOpacityProps> & React.RefAttributes<View>
>;

View File

@@ -0,0 +1,393 @@
/**
* 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.
*
* @flow strict-local
* @format
*/
import type {ViewStyleProp} from '../../StyleSheet/StyleSheet';
import type {TouchableWithoutFeedbackProps} from './TouchableWithoutFeedback';
import Animated from '../../Animated/Animated';
import Easing from '../../Animated/Easing';
import Pressability, {
type PressabilityConfig,
} from '../../Pressability/Pressability';
import {PressabilityDebugView} from '../../Pressability/PressabilityDebug';
import flattenStyle from '../../StyleSheet/flattenStyle';
import Platform from '../../Utilities/Platform';
import * as React from 'react';
export type TouchableOpacityTVProps = $ReadOnly<{
/**
* *(Apple TV only)* TV preferred focus (see documentation for the View component).
*
* @platform ios
* @deprecated Use `focusable` instead
*/
hasTVPreferredFocus?: ?boolean,
/**
* Designates the next view to receive focus when the user navigates down. See the Android documentation.
*
* @platform android
*/
nextFocusDown?: ?number,
/**
* Designates the next view to receive focus when the user navigates forward. See the Android documentation.
*
* @platform android
*/
nextFocusForward?: ?number,
/**
* Designates the next view to receive focus when the user navigates left. See the Android documentation.
*
* @platform android
*/
nextFocusLeft?: ?number,
/**
* Designates the next view to receive focus when the user navigates right. See the Android documentation.
*
* @platform android
*/
nextFocusRight?: ?number,
/**
* Designates the next view to receive focus when the user navigates up. See the Android documentation.
*
* @platform android
*/
nextFocusUp?: ?number,
}>;
type TouchableOpacityBaseProps = $ReadOnly<{
/**
* Determines what the opacity of the wrapped view should be when touch is active.
* Defaults to 0.2
*/
activeOpacity?: ?number,
style?: ?Animated.WithAnimatedValue<ViewStyleProp>,
hostRef?: ?React.RefSetter<React.ElementRef<typeof Animated.View>>,
}>;
export type TouchableOpacityProps = $ReadOnly<{
...TouchableWithoutFeedbackProps,
...TouchableOpacityTVProps,
...TouchableOpacityBaseProps,
}>;
type TouchableOpacityState = $ReadOnly<{
anim: Animated.Value,
pressability: Pressability,
}>;
/**
* A wrapper for making views respond properly to touches.
* On press down, the opacity of the wrapped view is decreased, dimming it.
*
* Opacity is controlled by wrapping the children in an Animated.View, which is
* added to the view hierarchy. Be aware that this can affect layout.
*
* Example:
*
* ```
* renderButton: function() {
* return (
* <TouchableOpacity onPress={this._onPressButton}>
* <Image
* style={styles.button}
* source={require('./myButton.png')}
* />
* </TouchableOpacity>
* );
* },
* ```
* ### Example
*
* ```ReactNativeWebPlayer
* import React, { Component } from 'react'
* import {
* AppRegistry,
* StyleSheet,
* TouchableOpacity,
* Text,
* View,
* } from 'react-native'
*
* class App extends Component {
* state = { count: 0 }
*
* onPress = () => {
* this.setState(state => ({
* count: state.count + 1
* }));
* };
*
* render() {
* return (
* <View style={styles.container}>
* <TouchableOpacity
* style={styles.button}
* onPress={this.onPress}>
* <Text> Touch Here </Text>
* </TouchableOpacity>
* <View style={[styles.countContainer]}>
* <Text style={[styles.countText]}>
* { this.state.count !== 0 ? this.state.count: null}
* </Text>
* </View>
* </View>
* )
* }
* }
*
* const styles = StyleSheet.create({
* container: {
* flex: 1,
* justifyContent: 'center',
* paddingHorizontal: 10
* },
* button: {
* alignItems: 'center',
* backgroundColor: '#DDDDDD',
* padding: 10
* },
* countContainer: {
* alignItems: 'center',
* padding: 10
* },
* countText: {
* color: '#FF00FF'
* }
* })
*
* AppRegistry.registerComponent('App', () => App)
* ```
*
*/
class TouchableOpacity extends React.Component<
TouchableOpacityProps,
TouchableOpacityState,
> {
state: TouchableOpacityState = {
anim: new Animated.Value(this._getChildStyleOpacityWithDefault()),
pressability: new Pressability(this._createPressabilityConfig()),
};
_createPressabilityConfig(): PressabilityConfig {
return {
cancelable: !this.props.rejectResponderTermination,
disabled:
this.props.disabled ??
this.props['aria-disabled'] ??
this.props.accessibilityState?.disabled,
hitSlop: this.props.hitSlop,
delayLongPress: this.props.delayLongPress,
delayPressIn: this.props.delayPressIn,
delayPressOut: this.props.delayPressOut,
minPressDuration: 0,
pressRectOffset: this.props.pressRetentionOffset,
onBlur: event => {
if (Platform.isTV) {
this._opacityInactive(250);
}
if (this.props.onBlur != null) {
this.props.onBlur(event);
}
},
onFocus: event => {
if (Platform.isTV) {
this._opacityActive(150);
}
if (this.props.onFocus != null) {
this.props.onFocus(event);
}
},
onLongPress: this.props.onLongPress,
onPress: this.props.onPress,
onPressIn: event => {
this._opacityActive(
event.dispatchConfig.registrationName === 'onResponderGrant'
? 0
: 150,
);
if (this.props.onPressIn != null) {
this.props.onPressIn(event);
}
},
onPressOut: event => {
this._opacityInactive(250);
if (this.props.onPressOut != null) {
this.props.onPressOut(event);
}
},
};
}
/**
* Animate the touchable to a new opacity.
*/
_setOpacityTo(toValue: number, duration: number): void {
Animated.timing(this.state.anim, {
toValue,
duration,
easing: Easing.inOut(Easing.quad),
useNativeDriver: true,
}).start();
}
_opacityActive(duration: number): void {
this._setOpacityTo(this.props.activeOpacity ?? 0.2, duration);
}
_opacityInactive(duration: number): void {
this._setOpacityTo(this._getChildStyleOpacityWithDefault(), duration);
}
_getChildStyleOpacityWithDefault(): number {
// $FlowFixMe[underconstrained-implicit-instantiation]
// $FlowFixMe[prop-missing]
const opacity = flattenStyle(this.props.style)?.opacity;
return typeof opacity === 'number' ? opacity : 1;
}
render(): React.Node {
// BACKWARD-COMPATIBILITY: Focus and blur events were never supported before
// adopting `Pressability`, so preserve that behavior.
const {onBlur, onFocus, ...eventHandlersWithoutBlurAndFocus} =
this.state.pressability.getEventHandlers();
let _accessibilityState = {
busy: this.props['aria-busy'] ?? this.props.accessibilityState?.busy,
checked:
this.props['aria-checked'] ?? this.props.accessibilityState?.checked,
disabled:
this.props['aria-disabled'] ?? this.props.accessibilityState?.disabled,
expanded:
this.props['aria-expanded'] ?? this.props.accessibilityState?.expanded,
selected:
this.props['aria-selected'] ?? this.props.accessibilityState?.selected,
};
_accessibilityState =
this.props.disabled != null
? {
..._accessibilityState,
disabled: this.props.disabled,
}
: _accessibilityState;
const accessibilityValue = {
max: this.props['aria-valuemax'] ?? this.props.accessibilityValue?.max,
min: this.props['aria-valuemin'] ?? this.props.accessibilityValue?.min,
now: this.props['aria-valuenow'] ?? this.props.accessibilityValue?.now,
text: this.props['aria-valuetext'] ?? this.props.accessibilityValue?.text,
};
const accessibilityLiveRegion =
this.props['aria-live'] === 'off'
? 'none'
: (this.props['aria-live'] ?? this.props.accessibilityLiveRegion);
const accessibilityLabel =
this.props['aria-label'] ?? this.props.accessibilityLabel;
return (
<Animated.View
accessible={this.props.accessible !== false}
accessibilityLabel={accessibilityLabel}
accessibilityHint={this.props.accessibilityHint}
accessibilityLanguage={this.props.accessibilityLanguage}
accessibilityRole={this.props.accessibilityRole}
accessibilityState={_accessibilityState}
accessibilityActions={this.props.accessibilityActions}
onAccessibilityAction={this.props.onAccessibilityAction}
accessibilityValue={accessibilityValue}
importantForAccessibility={
this.props['aria-hidden'] === true
? 'no-hide-descendants'
: // $FlowFixMe[incompatible-type] - AnimatedProps types were made more strict and need refining at this call site
this.props.importantForAccessibility
}
accessibilityViewIsModal={
this.props['aria-modal'] ?? this.props.accessibilityViewIsModal
}
accessibilityLiveRegion={accessibilityLiveRegion}
accessibilityElementsHidden={
this.props['aria-hidden'] ?? this.props.accessibilityElementsHidden
}
style={[this.props.style, {opacity: this.state.anim}]}
nativeID={this.props.id ?? this.props.nativeID}
testID={this.props.testID}
onLayout={this.props.onLayout}
nextFocusDown={this.props.nextFocusDown}
nextFocusForward={this.props.nextFocusForward}
nextFocusLeft={this.props.nextFocusLeft}
nextFocusRight={this.props.nextFocusRight}
nextFocusUp={this.props.nextFocusUp}
hasTVPreferredFocus={this.props.hasTVPreferredFocus}
hitSlop={this.props.hitSlop}
focusable={
this.props.focusable !== false &&
this.props.onPress !== undefined &&
!this.props.disabled
}
// $FlowFixMe[prop-missing]
ref={this.props.hostRef}
{...eventHandlersWithoutBlurAndFocus}>
{this.props.children}
{__DEV__ ? (
<PressabilityDebugView color="cyan" hitSlop={this.props.hitSlop} />
) : null}
</Animated.View>
);
}
componentDidUpdate(
prevProps: TouchableOpacityProps,
prevState: TouchableOpacityState,
) {
this.state.pressability.configure(this._createPressabilityConfig());
if (
this.props.disabled !== prevProps.disabled ||
// $FlowFixMe[underconstrained-implicit-instantiation]
// $FlowFixMe[prop-missing]
flattenStyle(prevProps.style)?.opacity !==
// $FlowFixMe[underconstrained-implicit-instantiation]
// $FlowFixMe[prop-missing]
flattenStyle(this.props.style)?.opacity
) {
this._opacityInactive(250);
}
}
componentDidMount(): void {
this.state.pressability.configure(this._createPressabilityConfig());
}
componentWillUnmount(): void {
this.state.pressability.reset();
this.state.anim.resetAnimation();
}
}
const Touchable: component(
ref?: React.RefSetter<React.ElementRef<typeof Animated.View>>,
...props: TouchableOpacityProps
) = ({
ref,
...props
}: {
ref?: React.RefSetter<React.ElementRef<typeof Animated.View>>,
...TouchableOpacityProps,
}) => <TouchableOpacity {...props} hostRef={ref} />;
Touchable.displayName = 'TouchableOpacity';
export default Touchable;

Some files were not shown because too many files have changed in this diff Show More