chore: add mobile app

This commit is contained in:
2026-02-07 22:33:02 +01:00
parent db34640acc
commit e89384ad4e
29327 changed files with 3886944 additions and 12 deletions
@@ -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.
*
* @flow
*/
'use client';
import canUseDOM from '../../modules/canUseDom';
function isScreenReaderEnabled(): Promise<*> {
return new Promise((resolve, reject) => {
resolve(true);
});
}
const prefersReducedMotionMedia =
canUseDOM && typeof window.matchMedia === 'function'
? window.matchMedia('(prefers-reduced-motion: reduce)')
: null;
function isReduceMotionEnabled(): Promise<*> {
return new Promise((resolve, reject) => {
resolve(
prefersReducedMotionMedia ? prefersReducedMotionMedia.matches : true
);
});
}
function addChangeListener(fn) {
if (prefersReducedMotionMedia != null) {
prefersReducedMotionMedia.addEventListener != null
? prefersReducedMotionMedia.addEventListener('change', fn)
: prefersReducedMotionMedia.addListener(fn);
}
}
function removeChangeListener(fn) {
if (prefersReducedMotionMedia != null) {
prefersReducedMotionMedia.removeEventListener != null
? prefersReducedMotionMedia.removeEventListener('change', fn)
: prefersReducedMotionMedia.removeListener(fn);
}
}
const handlers = {};
const AccessibilityInfo = {
/**
* 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.
*/
isScreenReaderEnabled,
/**
* Query whether the user prefers reduced motion.
*
* Returns a promise which resolves to a boolean.
* The result is `true` when a screen reader is enabled and `false` otherwise.
*/
isReduceMotionEnabled,
/**
* Deprecated
*/
fetch: isScreenReaderEnabled,
/**
* Add an event handler. Supported events: reduceMotionChanged
*/
addEventListener: function (eventName: string, handler: Function): Object {
if (eventName === 'reduceMotionChanged') {
if (!prefersReducedMotionMedia) {
return;
}
const listener = (event) => {
handler(event.matches);
};
addChangeListener(listener);
handlers[handler] = listener;
}
return {
remove: () => AccessibilityInfo.removeEventListener(eventName, handler)
};
},
/**
* Set accessibility focus to a react component.
*/
setAccessibilityFocus: function (reactTag: number): void {},
/**
* Post a string to be announced by the screen reader.
*/
announceForAccessibility: function (announcement: string): void {},
/**
* Remove an event handler.
*/
removeEventListener: function (eventName: string, handler: Function): void {
if (eventName === 'reduceMotionChanged') {
const listener = handlers[handler];
if (!listener || !prefersReducedMotionMedia) {
return;
}
removeChangeListener(listener);
}
return;
}
};
export default AccessibilityInfo;
@@ -0,0 +1,119 @@
/**
* Copyright (c) Nicolas Gallagher.
* 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
*/
'use client';
import type { ViewProps } from '../View';
import * as React from 'react';
import StyleSheet from '../StyleSheet';
import View from '../View';
const createSvgCircle = (style) => (
<circle cx="16" cy="16" fill="none" r="14" strokeWidth="4" style={style} />
);
type ActivityIndicatorProps = {
...ViewProps,
animating?: boolean,
color?: ?string,
hidesWhenStopped?: boolean,
size?: 'small' | 'large' | number
};
const ActivityIndicator: React.AbstractComponent<
ActivityIndicatorProps,
React.ElementRef<typeof View>
> = React.forwardRef((props, forwardedRef) => {
const {
animating = true,
color = '#1976D2',
hidesWhenStopped = true,
size = 'small',
style,
...other
} = props;
const svg = (
<svg height="100%" viewBox="0 0 32 32" width="100%">
{createSvgCircle({
stroke: color,
opacity: 0.2
})}
{createSvgCircle({
stroke: color,
strokeDasharray: 80,
strokeDashoffset: 60
})}
</svg>
);
return (
<View
{...other}
aria-valuemax={1}
aria-valuemin={0}
ref={forwardedRef}
role="progressbar"
style={[styles.container, style]}
>
<View
children={svg}
style={[
typeof size === 'number'
? { height: size, width: size }
: indicatorSizes[size],
styles.animation,
!animating && styles.animationPause,
!animating && hidesWhenStopped && styles.hidesWhenStopped
]}
/>
</View>
);
});
ActivityIndicator.displayName = 'ActivityIndicator';
const styles = StyleSheet.create({
container: {
alignItems: 'center',
justifyContent: 'center'
},
hidesWhenStopped: {
visibility: 'hidden'
},
animation: {
animationDuration: '0.75s',
animationKeyframes: [
{
'0%': { transform: 'rotate(0deg)' },
'100%': { transform: 'rotate(360deg)' }
}
],
animationTimingFunction: 'linear',
animationIterationCount: 'infinite'
},
animationPause: {
animationPlayState: 'paused'
}
});
const indicatorSizes = StyleSheet.create({
small: {
width: 20,
height: 20
},
large: {
width: 36,
height: 36
}
});
export default ActivityIndicator;
@@ -0,0 +1,14 @@
/**
* Copyright (c) Nicolas Gallagher.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
class Alert {
static alert() {}
}
export default Alert;
@@ -0,0 +1,13 @@
/**
* Copyright (c) Nicolas Gallagher.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
'use client';
import Animated from '../../vendor/react-native/Animated/Animated';
export default Animated;
@@ -0,0 +1,55 @@
/**
* Copyright (c) Nicolas Gallagher.
* 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
*/
import * as React from 'react';
import StyleSheet from '../StyleSheet';
import View from '../View';
type Props = {
WrapperComponent?: ?React.ComponentType<*>,
// $FlowFixMe
children?: React.Children,
rootTag: any
};
const RootTagContext: React.Context<any> = React.createContext(null);
const AppContainer: React.AbstractComponent<Props> = React.forwardRef(
(props: Props, forwardedRef?: React.Ref<any>) => {
const { children, WrapperComponent } = props;
let innerView = (
<View children={children} key={1} style={styles.appContainer} />
);
if (WrapperComponent) {
innerView = <WrapperComponent>{innerView}</WrapperComponent>;
}
return (
<RootTagContext.Provider value={props.rootTag}>
<View ref={forwardedRef} style={styles.appContainer}>
{innerView}
</View>
</RootTagContext.Provider>
);
}
);
AppContainer.displayName = 'AppContainer';
export default AppContainer;
const styles = StyleSheet.create({
appContainer: {
flex: 1,
pointerEvents: 'box-none'
}
});
@@ -0,0 +1,151 @@
/**
* Copyright (c) Nicolas Gallagher.
* 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
*/
'use client';
import type { Application } from './renderApplication';
import type { ComponentType, Node } from 'react';
import invariant from 'fbjs/lib/invariant';
import unmountComponentAtNode from '../unmountComponentAtNode';
import renderApplication, { getApplication } from './renderApplication';
type AppParams = Object;
type Runnable = {|
getApplication?: (AppParams) => {|
element: Node,
getStyleElement: (any) => Node
|},
run: (AppParams) => any
|};
export type ComponentProvider = () => ComponentType<any>;
export type ComponentProviderInstrumentationHook = (
component: ComponentProvider
) => ComponentType<any>;
export type WrapperComponentProvider = (any) => ComponentType<*>;
export type AppConfig = {
appKey: string,
component?: ComponentProvider,
run?: Function,
section?: boolean
};
const emptyObject = {};
const runnables: {| [appKey: string]: Runnable |} = {};
let componentProviderInstrumentationHook: ComponentProviderInstrumentationHook =
(component: ComponentProvider) => component();
let wrapperComponentProvider: ?WrapperComponentProvider;
/**
* `AppRegistry` is the JS entry point to running all React Native apps.
*/
export default class AppRegistry {
static getAppKeys(): Array<string> {
return Object.keys(runnables);
}
static getApplication(
appKey: string,
appParameters?: AppParams
): {| element: Node, getStyleElement: (any) => Node |} {
invariant(
runnables[appKey] && runnables[appKey].getApplication,
`Application ${appKey} has not been registered. ` +
'This is either due to an import error during initialization or failure to call AppRegistry.registerComponent.'
);
return runnables[appKey].getApplication(appParameters);
}
static registerComponent(
appKey: string,
componentProvider: ComponentProvider
): string {
runnables[appKey] = {
getApplication: (appParameters) =>
getApplication(
componentProviderInstrumentationHook(componentProvider),
appParameters ? appParameters.initialProps : emptyObject,
wrapperComponentProvider && wrapperComponentProvider(appParameters)
),
run: (appParameters): Application =>
renderApplication(
componentProviderInstrumentationHook(componentProvider),
wrapperComponentProvider && wrapperComponentProvider(appParameters),
appParameters.callback,
{
hydrate: appParameters.hydrate || false,
initialProps: appParameters.initialProps || emptyObject,
mode: appParameters.mode || 'concurrent',
rootTag: appParameters.rootTag
}
)
};
return appKey;
}
static registerConfig(config: Array<AppConfig>) {
config.forEach(({ appKey, component, run }) => {
if (run) {
AppRegistry.registerRunnable(appKey, run);
} else {
invariant(component, 'No component provider passed in');
AppRegistry.registerComponent(appKey, component);
}
});
}
// TODO: fix style sheet creation when using this method
static registerRunnable(appKey: string, run: Function): string {
runnables[appKey] = { run };
return appKey;
}
static runApplication(appKey: string, appParameters: Object): Application {
const isDevelopment =
process.env.NODE_ENV !== 'production' && process.env.NODE_ENV !== 'test';
if (isDevelopment) {
const params = { ...appParameters };
params.rootTag = `#${params.rootTag.id}`;
console.log(
`Running application "${appKey}" with appParams:\n`,
params,
`\nDevelopment-level warnings: ${isDevelopment ? 'ON' : 'OFF'}.` +
`\nPerformance optimizations: ${isDevelopment ? 'OFF' : 'ON'}.`
);
}
invariant(
runnables[appKey] && runnables[appKey].run,
`Application "${appKey}" has not been registered. ` +
'This is either due to an import error during initialization or failure to call AppRegistry.registerComponent.'
);
return runnables[appKey].run(appParameters);
}
static setComponentProviderInstrumentationHook(
hook: ComponentProviderInstrumentationHook
) {
componentProviderInstrumentationHook = hook;
}
static setWrapperComponentProvider(provider: WrapperComponentProvider) {
wrapperComponentProvider = provider;
}
static unmountApplicationComponentAtRootTag(rootTag: Object) {
unmountComponentAtNode(rootTag);
}
}
@@ -0,0 +1,72 @@
/**
* Copyright (c) Nicolas Gallagher.
* 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
*/
import type { ComponentType, Node } from 'react';
import AppContainer from './AppContainer';
import invariant from 'fbjs/lib/invariant';
import render, { hydrate } from '../render';
import StyleSheet from '../StyleSheet';
import React from 'react';
export type Application = {
unmount: () => void
};
export default function renderApplication<Props: Object>(
RootComponent: ComponentType<Props>,
WrapperComponent?: ?ComponentType<*>,
callback?: () => void,
options: {
hydrate: boolean,
initialProps: Props,
rootTag: any
}
): Application {
const { hydrate: shouldHydrate, initialProps, rootTag } = options;
const renderFn = shouldHydrate ? hydrate : render;
invariant(rootTag, 'Expect to have a valid rootTag, instead got ', rootTag);
return renderFn(
<AppContainer
WrapperComponent={WrapperComponent}
ref={callback}
rootTag={rootTag}
>
<RootComponent {...initialProps} />
</AppContainer>,
rootTag
);
}
export function getApplication(
RootComponent: ComponentType<Object>,
initialProps: Object,
WrapperComponent?: ?ComponentType<*>
): {| element: Node, getStyleElement: (Object) => Node |} {
const element = (
<AppContainer WrapperComponent={WrapperComponent} rootTag={{}}>
<RootComponent {...initialProps} />
</AppContainer>
);
// Don't escape CSS text
const getStyleElement = (props) => {
const sheet = StyleSheet.getSheet();
return (
<style
{...props}
dangerouslySetInnerHTML={{ __html: sheet.textContent }}
id={sheet.id}
/>
);
};
return { element, getStyleElement };
}
@@ -0,0 +1,81 @@
/**
* Copyright (c) Nicolas Gallagher.
* 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.
*
* @noflow
*/
'use client';
import invariant from 'fbjs/lib/invariant';
import EventEmitter from '../../vendor/react-native/vendor/emitter/EventEmitter';
import canUseDOM from '../../modules/canUseDom';
// Android 4.4 browser
const isPrefixed =
canUseDOM &&
!document.hasOwnProperty('hidden') &&
document.hasOwnProperty('webkitHidden');
const EVENT_TYPES = ['change', 'memoryWarning'];
const VISIBILITY_CHANGE_EVENT = isPrefixed
? 'webkitvisibilitychange'
: 'visibilitychange';
const VISIBILITY_STATE_PROPERTY = isPrefixed
? 'webkitVisibilityState'
: 'visibilityState';
const AppStates = {
BACKGROUND: 'background',
ACTIVE: 'active'
};
let changeEmitter = null;
export default class AppState {
static isAvailable = canUseDOM && !!document[VISIBILITY_STATE_PROPERTY];
static get currentState() {
if (!AppState.isAvailable) {
return AppStates.ACTIVE;
}
switch (document[VISIBILITY_STATE_PROPERTY]) {
case 'hidden':
case 'prerender':
case 'unloaded':
return AppStates.BACKGROUND;
default:
return AppStates.ACTIVE;
}
}
static addEventListener(type: string, handler: Function) {
if (AppState.isAvailable) {
invariant(
EVENT_TYPES.indexOf(type) !== -1,
'Trying to subscribe to unknown event: "%s"',
type
);
if (type === 'change') {
if (!changeEmitter) {
changeEmitter = new EventEmitter();
document.addEventListener(
VISIBILITY_CHANGE_EVENT,
() => {
if (changeEmitter) {
changeEmitter.emit('change', AppState.currentState);
}
},
false
);
}
return changeEmitter.addListener(type, handler);
}
}
}
}
@@ -0,0 +1,65 @@
/**
* Copyright (c) Nicolas Gallagher.
* 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
*/
'use client';
import canUseDOM from '../../modules/canUseDom';
export type ColorSchemeName = 'light' | 'dark';
export type AppearancePreferences = {|
colorScheme: ColorSchemeName
|};
type AppearanceListener = (preferences: AppearancePreferences) => void;
type DOMAppearanceListener = (ev: MediaQueryListEvent) => any;
function getQuery(): MediaQueryList | null {
return canUseDOM && window.matchMedia != null
? window.matchMedia('(prefers-color-scheme: dark)')
: null;
}
const query = getQuery();
const listenerMapping = new WeakMap<
AppearanceListener,
DOMAppearanceListener
>();
const Appearance = {
getColorScheme(): ColorSchemeName {
return query && query.matches ? 'dark' : 'light';
},
addChangeListener(listener: AppearanceListener): { remove: () => void } {
let mappedListener = listenerMapping.get(listener);
if (!mappedListener) {
mappedListener = ({ matches }: MediaQueryListEvent) => {
listener({ colorScheme: matches ? 'dark' : 'light' });
};
listenerMapping.set(listener, mappedListener);
}
if (query) {
query.addListener(mappedListener);
}
function remove(): void {
const mappedListener = listenerMapping.get(listener);
if (query && mappedListener) {
query.removeListener(mappedListener);
}
listenerMapping.delete(listener);
}
return { remove };
}
};
export default Appearance;
@@ -0,0 +1,26 @@
/**
* Copyright (c) Nicolas Gallagher.
* 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
*/
function emptyFunction() {}
const BackHandler = {
exitApp: emptyFunction,
addEventListener(): {| remove: () => void |} {
console.error(
'BackHandler is not supported on web and should not be used.'
);
return {
remove: emptyFunction
};
},
removeEventListener: emptyFunction
};
export default BackHandler;
@@ -0,0 +1,80 @@
/**
* Copyright (c) Nicolas Gallagher.
* 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
*/
import * as React from 'react';
import StyleSheet from '../StyleSheet';
import TouchableOpacity from '../TouchableOpacity';
import Text from '../Text';
//import { warnOnce } from '../../modules/warnOnce';
type ButtonProps = {|
accessibilityLabel?: ?string,
color?: ?string,
disabled?: boolean,
onPress?: ?(e: any) => void,
testID?: ?string,
title: string
|};
const Button: React.AbstractComponent<
ButtonProps,
React.ElementRef<typeof TouchableOpacity>
> = React.forwardRef((props, forwardedRef) => {
// warnOnce('Button', 'Button is deprecated. Please use Pressable.');
const { accessibilityLabel, color, disabled, onPress, testID, title } = props;
return (
<TouchableOpacity
accessibilityLabel={accessibilityLabel}
accessibilityRole="button"
disabled={disabled}
focusable={!disabled}
onPress={onPress}
ref={forwardedRef}
style={[
styles.button,
color && { backgroundColor: color },
disabled && styles.buttonDisabled
]}
testID={testID}
>
<Text style={[styles.text, disabled && styles.textDisabled]}>
{title}
</Text>
</TouchableOpacity>
);
});
Button.displayName = 'Button';
const styles = StyleSheet.create({
button: {
backgroundColor: '#2196F3',
borderRadius: 2
},
text: {
color: '#fff',
fontWeight: '500',
padding: 8,
textAlign: 'center',
textTransform: 'uppercase'
},
buttonDisabled: {
backgroundColor: '#dfdfdf'
},
textDisabled: {
color: '#a1a1a1'
}
});
export type { ButtonProps };
export default Button;
@@ -0,0 +1,143 @@
/**
* Copyright (c) Nicolas Gallagher.
* 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
*/
'use client';
import type { ColorValue } from '../../types';
import type { ViewProps } from '../View';
import * as React from 'react';
import createElement from '../createElement';
import StyleSheet from '../StyleSheet';
import View from '../View';
type CheckBoxProps = {
...ViewProps,
color?: ?ColorValue,
disabled?: boolean,
onChange?: ?(e: any) => void,
onValueChange?: ?(e: any) => void,
readOnly?: boolean,
value?: boolean
};
const CheckBox: React.AbstractComponent<
CheckBoxProps,
React.ElementRef<typeof View>
> = React.forwardRef((props, forwardedRef) => {
const {
'aria-readonly': ariaReadOnly,
color,
disabled,
onChange,
onValueChange,
readOnly,
style,
value,
...other
} = props;
function handleChange(event: Object) {
const value = event.nativeEvent.target.checked;
event.nativeEvent.value = value;
onChange && onChange(event);
onValueChange && onValueChange(value);
}
const fakeControl = (
<View
style={[
styles.fakeControl,
value && styles.fakeControlChecked,
// custom color
value && color && { backgroundColor: color, borderColor: color },
disabled && styles.fakeControlDisabled,
value && disabled && styles.fakeControlCheckedAndDisabled
]}
/>
);
const nativeControl = createElement('input', {
checked: value,
disabled: disabled,
onChange: handleChange,
readOnly:
readOnly === true ||
ariaReadOnly === true ||
other.accessibilityReadOnly === true,
ref: forwardedRef,
style: [styles.nativeControl, styles.cursorInherit],
type: 'checkbox'
});
return (
<View
{...other}
aria-disabled={disabled}
aria-readonly={ariaReadOnly}
style={[styles.root, style, disabled && styles.cursorDefault]}
>
{fakeControl}
{nativeControl}
</View>
);
});
CheckBox.displayName = 'CheckBox';
const styles = StyleSheet.create({
root: {
cursor: 'pointer',
height: 16,
userSelect: 'none',
width: 16
},
cursorDefault: {
cursor: 'default'
},
cursorInherit: {
cursor: 'inherit'
},
fakeControl: {
alignItems: 'center',
backgroundColor: '#fff',
borderColor: '#657786',
borderRadius: 2,
borderStyle: 'solid',
borderWidth: 2,
height: '100%',
justifyContent: 'center',
width: '100%'
},
fakeControlChecked: {
backgroundColor: '#009688',
backgroundImage:
'url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgdmVyc2lvbj0iMS4xIgogICB2aWV3Qm94PSIwIDAgMSAxIgogICBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWluWU1pbiBtZWV0Ij4KICA8cGF0aAogICAgIGQ9Ik0gMC4wNDAzODA1OSwwLjYyNjc3NjcgMC4xNDY0NDY2MSwwLjUyMDcxMDY4IDAuNDI5Mjg5MzIsMC44MDM1NTMzOSAwLjMyMzIyMzMsMC45MDk2MTk0MSB6IE0gMC4yMTcxNTcyOSwwLjgwMzU1MzM5IDAuODUzNTUzMzksMC4xNjcxNTcyOSAwLjk1OTYxOTQxLDAuMjczMjIzMyAwLjMyMzIyMzMsMC45MDk2MTk0MSB6IgogICAgIGlkPSJyZWN0Mzc4MCIKICAgICBzdHlsZT0iZmlsbDojZmZmZmZmO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lIiAvPgo8L3N2Zz4K")',
backgroundRepeat: 'no-repeat',
borderColor: '#009688'
},
fakeControlDisabled: {
borderColor: '#CCD6DD'
},
fakeControlCheckedAndDisabled: {
backgroundColor: '#AAB8C2',
borderColor: '#AAB8C2'
},
nativeControl: {
...StyleSheet.absoluteFillObject,
height: '100%',
margin: 0,
appearance: 'none',
padding: 0,
width: '100%'
}
});
export default CheckBox;
@@ -0,0 +1,63 @@
/**
* Copyright (c) Nicolas Gallagher.
* 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
*/
'use client';
let clipboardAvailable;
export default class Clipboard {
static isAvailable(): boolean {
if (clipboardAvailable === undefined) {
clipboardAvailable =
typeof document.queryCommandSupported === 'function' &&
document.queryCommandSupported('copy');
}
return clipboardAvailable;
}
static getString(): Promise<string> {
return Promise.resolve('');
}
static setString(text: string): boolean {
let success = false;
const body = document.body;
if (body) {
// add the text to a hidden node
const node = document.createElement('span');
node.textContent = text;
node.style.opacity = '0';
node.style.position = 'absolute';
node.style.whiteSpace = 'pre-wrap';
node.style.userSelect = 'auto';
body.appendChild(node);
// select the text
const selection = window.getSelection();
selection.removeAllRanges();
const range = document.createRange();
range.selectNodeContents(node);
selection.addRange(range);
// attempt to copy
try {
document.execCommand('copy');
success = true;
} catch (e) {}
// remove selection and node
selection.removeAllRanges();
body.removeChild(node);
}
return success;
}
}
@@ -0,0 +1,2 @@
import RCTDeviceEventEmitter from '../../vendor/react-native/EventEmitter/RCTDeviceEventEmitter';
export default RCTDeviceEventEmitter;
@@ -0,0 +1,160 @@
/**
* Copyright (c) Nicolas Gallagher.
* 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
*/
'use client';
import type { EventSubscription } from '../../vendor/react-native/vendor/emitter/EventEmitter';
import invariant from 'fbjs/lib/invariant';
import canUseDOM from '../../modules/canUseDom';
export type DisplayMetrics = {|
fontScale: number,
height: number,
scale: number,
width: number
|};
type DimensionsValue = {|
window: DisplayMetrics,
screen: DisplayMetrics
|};
type DimensionKey = 'window' | 'screen';
type DimensionEventListenerType = 'change';
const dimensions = {
window: {
fontScale: 1,
height: 0,
scale: 1,
width: 0
},
screen: {
fontScale: 1,
height: 0,
scale: 1,
width: 0
}
};
const listeners = {};
let shouldInit = canUseDOM;
function update() {
if (!canUseDOM) {
return;
}
const win = window;
let height;
let width;
/**
* iOS does not update viewport dimensions on keyboard open/close.
* window.visualViewport(https://developer.mozilla.org/en-US/docs/Web/API/VisualViewport)
* is used instead of document.documentElement.clientHeight (which remains as a fallback)
*/
if (win.visualViewport) {
const visualViewport = win.visualViewport;
/**
* We are multiplying by scale because height and width from visual viewport
* also react to pinch zoom, and become smaller when zoomed. But it is not desired
* behaviour, since originally documentElement client height and width were used,
* and they do not react to pinch zoom.
*/
height = Math.round(visualViewport.height * visualViewport.scale);
width = Math.round(visualViewport.width * visualViewport.scale);
} else {
const docEl = win.document.documentElement;
height = docEl.clientHeight;
width = docEl.clientWidth;
}
dimensions.window = {
fontScale: 1,
height,
scale: win.devicePixelRatio || 1,
width
};
dimensions.screen = {
fontScale: 1,
height: win.screen.height,
scale: win.devicePixelRatio || 1,
width: win.screen.width
};
}
function handleResize() {
update();
if (Array.isArray(listeners['change'])) {
listeners['change'].forEach((handler) => handler(dimensions));
}
}
export default class Dimensions {
static get(dimension: DimensionKey): DisplayMetrics {
if (shouldInit) {
shouldInit = false;
update();
}
invariant(dimensions[dimension], `No dimension set for key ${dimension}`);
return dimensions[dimension];
}
static set(initialDimensions: ?DimensionsValue): void {
if (initialDimensions) {
if (canUseDOM) {
invariant(false, 'Dimensions cannot be set in the browser');
} else {
if (initialDimensions.screen != null) {
dimensions.screen = initialDimensions.screen;
}
if (initialDimensions.window != null) {
dimensions.window = initialDimensions.window;
}
}
}
}
static addEventListener(
type: DimensionEventListenerType,
handler: (DimensionsValue) => void
): EventSubscription {
listeners[type] = listeners[type] || [];
listeners[type].push(handler);
return {
remove: () => {
this.removeEventListener(type, handler);
}
};
}
static removeEventListener(
type: DimensionEventListenerType,
handler: (DimensionsValue) => void
): void {
if (Array.isArray(listeners[type])) {
listeners[type] = listeners[type].filter(
(_handler) => _handler !== handler
);
}
}
}
if (canUseDOM) {
if (window.visualViewport) {
window.visualViewport.addEventListener('resize', handleResize, false);
} else {
window.addEventListener('resize', handleResize, false);
}
}
@@ -0,0 +1,11 @@
/**
* Copyright (c) Nicolas Gallagher.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import Easing from '../../vendor/react-native/Animated/Easing';
export default Easing;
@@ -0,0 +1,14 @@
/**
* Copyright (c) Nicolas Gallagher.
* 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
*/
'use client';
import FlatList from '../../vendor/react-native/FlatList';
export default FlatList;
@@ -0,0 +1,33 @@
/**
* Copyright (c) Nicolas Gallagher.
* 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
*/
type I18nManagerStatus = {
allowRTL: (allowRTL: boolean) => void,
forceRTL: (forceRTL: boolean) => void,
getConstants: () => Constants
};
type Constants = {
isRTL: boolean
};
const I18nManager: I18nManagerStatus = {
allowRTL() {
return;
},
forceRTL() {
return;
},
getConstants(): Constants {
return { isRTL: false };
}
};
export default I18nManager;
+443
View File
@@ -0,0 +1,443 @@
/**
* Copyright (c) Nicolas Gallagher.
* 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
*/
'use client';
import type { ImageProps } from './types';
import * as React from 'react';
import createElement from '../createElement';
import { getAssetByID } from '../../modules/AssetRegistry';
import { createBoxShadowValue } from '../StyleSheet/preprocess';
import ImageLoader from '../../modules/ImageLoader';
import PixelRatio from '../PixelRatio';
import StyleSheet from '../StyleSheet';
import TextAncestorContext from '../Text/TextAncestorContext';
import View from '../View';
import { warnOnce } from '../../modules/warnOnce';
export type { ImageProps };
const ERRORED = 'ERRORED';
const LOADED = 'LOADED';
const LOADING = 'LOADING';
const IDLE = 'IDLE';
let _filterId = 0;
const svgDataUriPattern = /^(data:image\/svg\+xml;utf8,)(.*)/;
function createTintColorSVG(tintColor, id) {
return tintColor && id != null ? (
<svg
style={{
position: 'absolute',
height: 0,
visibility: 'hidden',
width: 0
}}
>
<defs>
<filter id={`tint-${id}`} suppressHydrationWarning={true}>
<feFlood floodColor={`${tintColor}`} key={tintColor} />
<feComposite in2="SourceAlpha" operator="in" />
</filter>
</defs>
</svg>
) : null;
}
function extractNonStandardStyleProps(
style,
blurRadius,
filterId,
tintColorProp
) {
const flatStyle = StyleSheet.flatten(style);
const { filter, resizeMode, shadowOffset, tintColor } = flatStyle;
if (flatStyle.resizeMode) {
warnOnce(
'Image.style.resizeMode',
'Image: style.resizeMode is deprecated. Please use props.resizeMode.'
);
}
if (flatStyle.tintColor) {
warnOnce(
'Image.style.tintColor',
'Image: style.tintColor is deprecated. Please use props.tintColor.'
);
}
// Add CSS filters
// React Native exposes these features as props and proprietary styles
const filters = [];
let _filter = null;
if (filter) {
filters.push(filter);
}
if (blurRadius) {
filters.push(`blur(${blurRadius}px)`);
}
if (shadowOffset) {
const shadowString = createBoxShadowValue(flatStyle);
if (shadowString) {
filters.push(`drop-shadow(${shadowString})`);
}
}
if ((tintColorProp || tintColor) && filterId != null) {
filters.push(`url(#tint-${filterId})`);
}
if (filters.length > 0) {
_filter = filters.join(' ');
}
return [resizeMode, _filter, tintColor];
}
function resolveAssetDimensions(source) {
if (typeof source === 'number') {
const { height, width } = getAssetByID(source);
return { height, width };
} else if (
source != null &&
!Array.isArray(source) &&
typeof source === 'object'
) {
const { height, width } = source;
return { height, width };
}
}
function resolveAssetUri(source): ?string {
let uri = null;
if (typeof source === 'number') {
// get the URI from the packager
const asset = getAssetByID(source);
if (asset == null) {
throw new Error(
`Image: asset with ID "${source}" could not be found. Please check the image source or packager.`
);
}
let scale = asset.scales[0];
if (asset.scales.length > 1) {
const preferredScale = PixelRatio.get();
// Get the scale which is closest to the preferred scale
scale = asset.scales.reduce((prev, curr) =>
Math.abs(curr - preferredScale) < Math.abs(prev - preferredScale)
? curr
: prev
);
}
const scaleSuffix = scale !== 1 ? `@${scale}x` : '';
uri = asset
? `${asset.httpServerLocation}/${asset.name}${scaleSuffix}.${asset.type}`
: '';
} else if (typeof source === 'string') {
uri = source;
} else if (source && typeof source.uri === 'string') {
uri = source.uri;
}
if (uri) {
const match = uri.match(svgDataUriPattern);
// inline SVG markup may contain characters (e.g., #, ") that need to be escaped
if (match) {
const [, prefix, svg] = match;
const encodedSvg = encodeURIComponent(svg);
return `${prefix}${encodedSvg}`;
}
}
return uri;
}
interface ImageStatics {
getSize: (
uri: string,
success: (width: number, height: number) => void,
failure: () => void
) => void;
prefetch: (uri: string) => Promise<void>;
queryCache: (
uris: Array<string>
) => Promise<{| [uri: string]: 'disk/memory' |}>;
}
const Image: React.AbstractComponent<
ImageProps,
React.ElementRef<typeof View>
> = React.forwardRef((props, ref) => {
const {
'aria-label': _ariaLabel,
accessibilityLabel,
blurRadius,
defaultSource,
draggable,
onError,
onLayout,
onLoad,
onLoadEnd,
onLoadStart,
pointerEvents,
source,
style,
...rest
} = props;
const ariaLabel = _ariaLabel || accessibilityLabel;
if (process.env.NODE_ENV !== 'production') {
if (props.children) {
throw new Error(
'The <Image> component cannot contain children. If you want to render content on top of the image, consider using the <ImageBackground> component or absolute positioning.'
);
}
}
const [state, updateState] = React.useState(() => {
const uri = resolveAssetUri(source);
if (uri != null) {
const isLoaded = ImageLoader.has(uri);
if (isLoaded) {
return LOADED;
}
}
return IDLE;
});
const [layout, updateLayout] = React.useState({});
const hasTextAncestor = React.useContext(TextAncestorContext);
const hiddenImageRef = React.useRef(null);
const filterRef = React.useRef(_filterId++);
const requestRef = React.useRef(null);
const shouldDisplaySource =
state === LOADED || (state === LOADING && defaultSource == null);
const [_resizeMode, filter, _tintColor] = extractNonStandardStyleProps(
style,
blurRadius,
filterRef.current,
props.tintColor
);
const resizeMode = props.resizeMode || _resizeMode || 'cover';
const tintColor = props.tintColor || _tintColor;
const selectedSource = shouldDisplaySource ? source : defaultSource;
const displayImageUri = resolveAssetUri(selectedSource);
const imageSizeStyle = resolveAssetDimensions(selectedSource);
const backgroundImage = displayImageUri ? `url("${displayImageUri}")` : null;
const backgroundSize = getBackgroundSize();
// Accessibility image allows users to trigger the browser's image context menu
const hiddenImage = displayImageUri
? createElement('img', {
alt: ariaLabel || '',
style: styles.accessibilityImage$raw,
draggable: draggable || false,
ref: hiddenImageRef,
src: displayImageUri
})
: null;
function getBackgroundSize(): ?string {
if (
hiddenImageRef.current != null &&
(resizeMode === 'center' || resizeMode === 'repeat')
) {
const { naturalHeight, naturalWidth } = hiddenImageRef.current;
const { height, width } = layout;
if (naturalHeight && naturalWidth && height && width) {
const scaleFactor = Math.min(
1,
width / naturalWidth,
height / naturalHeight
);
const x = Math.ceil(scaleFactor * naturalWidth);
const y = Math.ceil(scaleFactor * naturalHeight);
return `${x}px ${y}px`;
}
}
}
function handleLayout(e) {
if (resizeMode === 'center' || resizeMode === 'repeat' || onLayout) {
const { layout } = e.nativeEvent;
onLayout && onLayout(e);
updateLayout(layout);
}
}
// Image loading
const uri = resolveAssetUri(source);
React.useEffect(() => {
abortPendingRequest();
if (uri != null) {
updateState(LOADING);
if (onLoadStart) {
onLoadStart();
}
requestRef.current = ImageLoader.load(
uri,
function load(e) {
updateState(LOADED);
if (onLoad) {
onLoad(e);
}
if (onLoadEnd) {
onLoadEnd();
}
},
function error() {
updateState(ERRORED);
if (onError) {
onError({
nativeEvent: {
error: `Failed to load resource ${uri}`
}
});
}
if (onLoadEnd) {
onLoadEnd();
}
}
);
}
function abortPendingRequest() {
if (requestRef.current != null) {
ImageLoader.abort(requestRef.current);
requestRef.current = null;
}
}
return abortPendingRequest;
}, [uri, requestRef, updateState, onError, onLoad, onLoadEnd, onLoadStart]);
return (
<View
{...rest}
aria-label={ariaLabel}
onLayout={handleLayout}
pointerEvents={pointerEvents}
ref={ref}
style={[
styles.root,
hasTextAncestor && styles.inline,
imageSizeStyle,
style,
styles.undo,
// TEMP: avoid deprecated shadow props regression
// until Image refactored to use createElement.
{ boxShadow: null }
]}
>
<View
style={[
styles.image,
resizeModeStyles[resizeMode],
{ backgroundImage, filter },
backgroundSize != null && { backgroundSize }
]}
suppressHydrationWarning={true}
/>
{hiddenImage}
{createTintColorSVG(tintColor, filterRef.current)}
</View>
);
});
Image.displayName = 'Image';
// $FlowIgnore: This is the correct type, but casting makes it unhappy since the variables aren't defined yet
const ImageWithStatics = (Image: React.AbstractComponent<
ImageProps,
React.ElementRef<typeof View>
> &
ImageStatics);
ImageWithStatics.getSize = function (uri, success, failure) {
ImageLoader.getSize(uri, success, failure);
};
ImageWithStatics.prefetch = function (uri) {
return ImageLoader.prefetch(uri);
};
ImageWithStatics.queryCache = function (uris) {
return ImageLoader.queryCache(uris);
};
const styles = StyleSheet.create({
root: {
flexBasis: 'auto',
overflow: 'hidden',
zIndex: 0
},
inline: {
display: 'inline-flex'
},
undo: {
// These styles are converted to CSS filters applied to the
// element displaying the background image.
blurRadius: null,
shadowColor: null,
shadowOpacity: null,
shadowOffset: null,
shadowRadius: null,
tintColor: null,
// These styles are not supported
overlayColor: null,
resizeMode: null
},
image: {
...StyleSheet.absoluteFillObject,
backgroundColor: 'transparent',
backgroundPosition: 'center',
backgroundRepeat: 'no-repeat',
backgroundSize: 'cover',
height: '100%',
width: '100%',
zIndex: -1
},
accessibilityImage$raw: {
...StyleSheet.absoluteFillObject,
height: '100%',
opacity: 0,
width: '100%',
zIndex: -1
}
});
const resizeModeStyles = StyleSheet.create({
center: {
backgroundSize: 'auto'
},
contain: {
backgroundSize: 'contain'
},
cover: {
backgroundSize: 'cover'
},
none: {
backgroundPosition: '0',
backgroundSize: 'auto'
},
repeat: {
backgroundPosition: '0',
backgroundRepeat: 'repeat',
backgroundSize: 'auto'
},
stretch: {
backgroundSize: '100% 100%'
}
});
export default ImageWithStatics;
+103
View File
@@ -0,0 +1,103 @@
/**
* Copyright (c) Nicolas Gallagher.
* 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
*/
import type { ColorValue, GenericStyleProp } from '../../types';
import type { ViewProps, ViewStyle } from '../View/types';
type SourceObject = {
/**
* `body` is the HTTP body to send with the request. This must be a valid
* UTF-8 string, and will be sent exactly as specified, with no
* additional encoding (e.g. URL-escaping or base64) applied.
*/
body?: string,
/**
* `cache` determines how the requests handles potentially cached
* responses.
*
* - `default`: Use the native platforms default strategy. `useProtocolCachePolicy` on iOS.
*
* - `reload`: The data for the URL will be loaded from the originating source.
* No existing cache data should be used to satisfy a URL load request.
*
* - `force-cache`: The existing cached data will be used to satisfy the request,
* regardless of its age or expiration date. If there is no existing data in the cache
* corresponding the request, the data is loaded from the originating source.
*
* - `only-if-cached`: The existing cache data will be used to satisfy a request, regardless of
* its age or expiration date. If there is no existing data in the cache corresponding
* to a URL load request, no attempt is made to load the data from the originating source,
* and the load is considered to have failed.
*
* @platform ios
*/
cache?: 'default' | 'reload' | 'force-cache' | 'only-if-cached',
/**
* `headers` is an object representing the HTTP headers to send along with the
* request for a remote image.
*/
headers?: { [key: string]: string },
/**
* `method` is the HTTP Method to use. Defaults to GET if not specified.
*/
method?: string,
/**
* `scale` is used to indicate the scale factor of the image. Defaults to 1.0 if
* unspecified, meaning that one image pixel equates to one display point / DIP.
*/
scale?: number,
/**
* `uri` is a string representing the resource identifier for the image, which
* could be an http address, a local file path, or the name of a static image
* resource (which should be wrapped in the `require('./path/to/image.png')`
* function).
*/
uri: string,
/**
* `width` and `height` can be specified if known at build time, in which case
* these will be used to set the default `<Image/>` component dimensions.
*/
height?: number,
width?: number
};
export type ResizeMode =
| 'center'
| 'contain'
| 'cover'
| 'none'
| 'repeat'
| 'stretch';
export type Source = number | string | SourceObject | Array<SourceObject>;
export type ImageStyle = {
...ViewStyle,
// @deprecated
resizeMode?: ResizeMode,
tintColor?: ColorValue
};
export type ImageProps = {
...ViewProps,
blurRadius?: number,
defaultSource?: Source,
draggable?: boolean,
onError?: (e: any) => void,
onLayout?: (e: any) => void,
onLoad?: (e: any) => void,
onLoadEnd?: (e: any) => void,
onLoadStart?: (e: any) => void,
onProgress?: (e: any) => void,
resizeMode?: ResizeMode,
source?: Source,
style?: GenericStyleProp<ImageStyle>,
tintColor?: ColorValue
};
@@ -0,0 +1,73 @@
/**
* 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
*/
import type { ImageProps } from '../Image';
import type { ViewProps } from '../View';
import * as React from 'react';
import { forwardRef } from 'react';
import Image from '../Image';
import StyleSheet from '../StyleSheet';
import View from '../View';
type ImageBackgroundProps = {
...ImageProps,
imageRef?: any,
imageStyle?: $PropertyType<ImageProps, 'style'>,
style?: $PropertyType<ViewProps, 'style'>
};
const emptyObject = {};
/**
* Very simple drop-in replacement for <Image> which supports nesting views.
*/
const ImageBackground: React.AbstractComponent<
ImageBackgroundProps,
React.ElementRef<typeof View>
> = forwardRef((props, forwardedRef) => {
const {
children,
style = emptyObject,
imageStyle,
imageRef,
...rest
} = props;
const { height, width } = StyleSheet.flatten(style);
return (
<View ref={forwardedRef} style={style}>
<Image
{...rest}
ref={imageRef}
style={[
{
// Temporary Workaround:
// Current (imperfect yet) implementation of <Image> overwrites width and height styles
// (which is not quite correct), and these styles conflict with explicitly set styles
// of <ImageBackground> and with our internal layout model here.
// So, we have to proxy/reapply these styles explicitly for actual <Image> component.
// This workaround should be removed after implementing proper support of
// intrinsic content size of the <Image>.
width,
height,
zIndex: -1
},
StyleSheet.absoluteFill,
imageStyle
]}
/>
{children}
</View>
);
});
ImageBackground.displayName = 'ImageBackground';
export default ImageBackground;
@@ -0,0 +1,2 @@
import UnimplementedView from '../../modules/UnimplementedView';
export default UnimplementedView;
@@ -0,0 +1,115 @@
/**
* Copyright (c) Nicolas Gallagher.
* 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
*/
import invariant from 'fbjs/lib/invariant';
type SimpleTask = {|
name: string,
run: () => void
|};
type PromiseTask = {|
name: string,
gen: () => Promise<void>
|};
export type Task = SimpleTask | PromiseTask | (() => void);
class TaskQueue {
constructor({ onMoreTasks }: { onMoreTasks: () => void, ... }) {
this._onMoreTasks = onMoreTasks;
this._queueStack = [{ tasks: [], popable: true }];
}
enqueue(task: Task): void {
this._getCurrentQueue().push(task);
}
enqueueTasks(tasks: Array<Task>): void {
tasks.forEach((task) => this.enqueue(task));
}
cancelTasks(tasksToCancel: Array<Task>): void {
this._queueStack = this._queueStack
.map((queue) => ({
...queue,
tasks: queue.tasks.filter((task) => tasksToCancel.indexOf(task) === -1)
}))
.filter((queue, idx) => queue.tasks.length > 0 || idx === 0);
}
hasTasksToProcess(): boolean {
return this._getCurrentQueue().length > 0;
}
/**
* Executes the next task in the queue.
*/
processNext(): void {
const queue = this._getCurrentQueue();
if (queue.length) {
const task = queue.shift();
try {
if (typeof task === 'object' && task.gen) {
this._genPromise(task);
} else if (typeof task === 'object' && task.run) {
task.run();
} else {
invariant(
typeof task === 'function',
'Expected Function, SimpleTask, or PromiseTask, but got:\n' +
JSON.stringify(task, null, 2)
);
task();
}
} catch (e) {
e.message =
'TaskQueue: Error with task ' + (task.name || '') + ': ' + e.message;
throw e;
}
}
}
_queueStack: Array<{
tasks: Array<Task>,
popable: boolean,
...
}>;
_onMoreTasks: () => void;
_getCurrentQueue(): Array<Task> {
const stackIdx = this._queueStack.length - 1;
const queue = this._queueStack[stackIdx];
if (queue.popable && queue.tasks.length === 0 && stackIdx > 0) {
this._queueStack.pop();
return this._getCurrentQueue();
} else {
return queue.tasks;
}
}
_genPromise(task: PromiseTask) {
const length = this._queueStack.push({ tasks: [], popable: false });
const stackIdx = length - 1;
const stackItem = this._queueStack[stackIdx];
task
.gen()
.then(() => {
stackItem.popable = true;
this.hasTasksToProcess() && this._onMoreTasks();
})
.catch((ex) => {
setTimeout(() => {
ex.message = `TaskQueue: Error resolving Promise in task ${task.name}: ${ex.message}`;
throw ex;
}, 0);
});
}
}
export default TaskQueue;
@@ -0,0 +1,142 @@
/**
* Copyright (c) Nicolas Gallagher.
* 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
*/
import invariant from 'fbjs/lib/invariant';
import type { Task } from './TaskQueue';
import TaskQueue from './TaskQueue';
import type { EventSubscription } from '../../vendor/react-native/vendor/emitter/EventEmitter';
import EventEmitter from '../../vendor/react-native/vendor/emitter/EventEmitter';
import requestIdleCallback from '../../modules/requestIdleCallback';
const _emitter = new EventEmitter<{
interactionComplete: [],
interactionStart: []
}>();
const InteractionManager = {
Events: {
interactionStart: 'interactionStart',
interactionComplete: 'interactionComplete'
},
/**
* Schedule a function to run after all interactions have completed.
*/
runAfterInteractions(task: ?Task): {
then: Function,
done: Function,
cancel: Function
} {
const tasks: Array<Task> = [];
const promise = new Promise((resolve) => {
_scheduleUpdate();
if (task) {
tasks.push(task);
}
tasks.push({
run: resolve,
name: 'resolve ' + ((task && task.name) || '?')
});
_taskQueue.enqueueTasks(tasks);
});
return {
then: promise.then.bind(promise),
done: promise.then.bind(promise),
cancel: () => {
_taskQueue.cancelTasks(tasks);
}
};
},
/**
* Notify manager that an interaction has started.
*/
createInteractionHandle(): number {
_scheduleUpdate();
const handle = ++_inc;
_addInteractionSet.add(handle);
return handle;
},
/**
* Notify manager that an interaction has completed.
*/
clearInteractionHandle(handle: number) {
invariant(!!handle, 'Must provide a handle to clear.');
_scheduleUpdate();
_addInteractionSet.delete(handle);
_deleteInteractionSet.add(handle);
},
addListener: (_emitter.addListener.bind(_emitter): EventSubscription),
/**
*
* @param deadline
*/
setDeadline(deadline: number) {
_deadline = deadline;
}
};
const _interactionSet = new Set();
const _addInteractionSet = new Set();
const _deleteInteractionSet = new Set();
const _taskQueue = new TaskQueue({ onMoreTasks: _scheduleUpdate });
let _nextUpdateHandle: TimeoutID | number = 0;
let _inc = 0;
let _deadline = -1;
/**
* Schedule an asynchronous update to the interaction state.
*/
function _scheduleUpdate() {
if (!_nextUpdateHandle) {
if (_deadline > 0) {
_nextUpdateHandle = setTimeout(_processUpdate);
} else {
_nextUpdateHandle = requestIdleCallback(_processUpdate);
}
}
}
/**
* Notify listeners, process queue, etc
*/
function _processUpdate() {
_nextUpdateHandle = 0;
const interactionCount = _interactionSet.size;
_addInteractionSet.forEach((handle) => _interactionSet.add(handle));
_deleteInteractionSet.forEach((handle) => _interactionSet.delete(handle));
const nextInteractionCount = _interactionSet.size;
if (interactionCount !== 0 && nextInteractionCount === 0) {
_emitter.emit(InteractionManager.Events.interactionComplete);
} else if (interactionCount === 0 && nextInteractionCount !== 0) {
_emitter.emit(InteractionManager.Events.interactionStart);
}
if (nextInteractionCount === 0) {
// It seems that we can't know the running time of the current event loop,
// we can only calculate the running time of the current task queue.
const begin = Date.now();
while (_taskQueue.hasTasksToProcess()) {
_taskQueue.processNext();
if (_deadline > 0 && Date.now() - begin >= _deadline) {
_scheduleUpdate();
break;
}
}
}
_addInteractionSet.clear();
_deleteInteractionSet.clear();
}
export default InteractionManager;
@@ -0,0 +1,28 @@
/**
* Copyright (c) Nicolas Gallagher.
* 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
*/
import dismissKeyboard from '../../modules/dismissKeyboard';
// in the future we can use https://github.com/w3c/virtual-keyboard
const Keyboard = {
isVisible(): boolean {
return false;
},
addListener(): {| remove: () => void |} {
return { remove: () => {} };
},
dismiss() {
dismissKeyboard();
},
removeAllListeners() {},
removeListener() {}
};
export default Keyboard;
@@ -0,0 +1,59 @@
/**
* Copyright (c) Nicolas Gallagher.
* 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
*/
'use client';
import type { LayoutEvent, LayoutValue } from '../../types';
import type { ViewProps } from '../View';
import * as React from 'react';
import View from '../View';
type KeyboardAvoidingViewProps = {
...ViewProps,
behavior?: 'height' | 'padding' | 'position',
contentContainerStyle?: $PropertyType<ViewProps, 'style'>,
keyboardVerticalOffset: number
};
class KeyboardAvoidingView extends React.Component<KeyboardAvoidingViewProps> {
frame: ?LayoutValue = null;
relativeKeyboardHeight(keyboardFrame: Object): number {
const frame = this.frame;
if (!frame || !keyboardFrame) {
return 0;
}
const keyboardY =
keyboardFrame.screenY - (this.props.keyboardVerticalOffset || 0);
return Math.max(frame.y + frame.height - keyboardY, 0);
}
onKeyboardChange(event: Object) {}
onLayout: (event: LayoutEvent) => void = (event: LayoutEvent) => {
this.frame = event.nativeEvent.layout;
};
render(): React.Node {
const {
/* eslint-disable */
behavior,
contentContainerStyle,
keyboardVerticalOffset,
/* eslint-enable */
...rest
} = this.props;
return <View onLayout={this.onLayout} {...rest} />;
}
}
export default KeyboardAvoidingView;
@@ -0,0 +1,12 @@
/**
* Copyright (c) Nicolas Gallagher.
* 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
*/
import LayoutAnimation from '../../vendor/react-native/LayoutAnimation';
export default LayoutAnimation;
@@ -0,0 +1,124 @@
/**
* Copyright (c) Nicolas Gallagher.
* 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
*/
import invariant from 'fbjs/lib/invariant';
import canUseDOM from '../../modules/canUseDom';
const initialURL = canUseDOM ? window.location.href : '';
type Callback = (...args: any) => void;
class Linking {
/**
* An object mapping of event name
* and all the callbacks subscribing to it
*/
_eventCallbacks: { [key: string]: Array<Callback> } = {};
_dispatchEvent(event: string, ...data: any) {
const listeners = this._eventCallbacks[event];
if (listeners != null && Array.isArray(listeners)) {
listeners.map((listener) => {
listener(...data);
});
}
}
/**
* Adds a event listener for the specified event. The callback will be called when the
* said event is dispatched.
*/
addEventListener(
eventType: string,
callback: Callback
): {| remove(): void |} {
const _this = this;
if (!_this._eventCallbacks[eventType]) {
_this._eventCallbacks[eventType] = [callback];
}
_this._eventCallbacks[eventType].push(callback);
return {
remove() {
const callbacks = _this._eventCallbacks[eventType];
const filteredCallbacks = callbacks.filter(
(c) => c.toString() !== callback.toString()
);
_this._eventCallbacks[eventType] = filteredCallbacks;
}
};
}
/**
* Removes a previously added event listener for the specified event. The callback must
* be the same object as the one passed to `addEventListener`.
*/
removeEventListener(eventType: string, callback: Callback): void {
console.error(
`Linking.removeEventListener('${eventType}', ...): Method has been ` +
'deprecated. Please instead use `remove()` on the subscription ' +
'returned by `Linking.addEventListener`.'
);
const callbacks = this._eventCallbacks[eventType];
const filteredCallbacks = callbacks.filter(
(c) => c.toString() !== callback.toString()
);
this._eventCallbacks[eventType] = filteredCallbacks;
}
canOpenURL(): Promise<boolean> {
return Promise.resolve(true);
}
getInitialURL(): Promise<string> {
return Promise.resolve(initialURL);
}
/**
* Try to open the given url in a secure fashion. The method returns a Promise object.
* If a target is passed (including undefined) that target will be used, otherwise '_blank'.
* If the url opens, the promise is resolved. If not, the promise is rejected.
* Dispatches the `onOpen` event if `url` is opened successfully.
*/
openURL(url: string, target?: string): Promise<Object | void> {
if (arguments.length === 1) {
target = '_blank';
}
try {
open(url, target);
this._dispatchEvent('onOpen', url);
return Promise.resolve();
} catch (e) {
return Promise.reject(e);
}
}
_validateURL(url: string) {
invariant(
typeof url === 'string',
'Invalid URL: should be a string. Was: ' + url
);
invariant(url, 'Invalid URL: cannot be empty');
}
}
const open = (url, target) => {
if (canUseDOM) {
const urlToOpen = new URL(url, window.location).toString();
if (urlToOpen.indexOf('tel:') === 0) {
window.location = urlToOpen;
} else {
window.open(urlToOpen, target, 'noopener');
}
}
};
export default (new Linking(): Linking);
@@ -0,0 +1,17 @@
/**
* Copyright (c) 2016-present, Nicolas Gallagher.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
const LogBox = {
ignoreLogs() {},
ignoreAllLogs() {},
uninstall() {},
install() {}
};
export default LogBox;
@@ -0,0 +1,164 @@
/**
* Copyright (c) Nicolas Gallagher.
* 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
*/
import * as React from 'react';
import StyleSheet from '../StyleSheet';
import createElement from '../createElement';
const ANIMATION_DURATION = 250;
function getAnimationStyle(animationType, visible) {
if (animationType === 'slide') {
return visible ? animatedSlideInStyles : animatedSlideOutStyles;
}
if (animationType === 'fade') {
return visible ? animatedFadeInStyles : animatedFadeOutStyles;
}
return visible ? styles.container : styles.hidden;
}
export type ModalAnimationProps = {|
animationType?: ?('none' | 'slide' | 'fade'),
children?: any,
onDismiss?: ?() => void,
onShow?: ?() => void,
visible?: ?boolean
|};
function ModalAnimation(props: ModalAnimationProps): React.Node {
const { animationType, children, onDismiss, onShow, visible } = props;
const [isRendering, setIsRendering] = React.useState(false);
const wasVisible = React.useRef(false);
const wasRendering = React.useRef(false);
const isAnimated = animationType && animationType !== 'none';
const animationEndCallback = React.useCallback(
(e: any) => {
if (e && e.currentTarget !== e.target) {
// If the event was generated for something NOT this element we
// should ignore it as it's not relevant to us
return;
}
if (visible) {
if (onShow) {
onShow();
}
} else {
setIsRendering(false);
}
},
[onShow, visible]
);
React.useEffect(() => {
if (wasRendering.current && !isRendering && onDismiss) {
onDismiss();
}
wasRendering.current = isRendering;
}, [isRendering, onDismiss]);
React.useEffect(() => {
if (visible) {
setIsRendering(true);
}
if (visible !== wasVisible.current && !isAnimated) {
// Manually call `animationEndCallback` if no animation is used
animationEndCallback();
}
wasVisible.current = visible;
}, [isAnimated, visible, animationEndCallback]);
return isRendering || visible
? createElement('div', {
style: isRendering
? getAnimationStyle(animationType, visible)
: styles.hidden,
onAnimationEnd: animationEndCallback,
children
})
: null;
}
const styles = StyleSheet.create({
container: {
position: 'fixed',
top: 0,
right: 0,
bottom: 0,
left: 0,
zIndex: 9999
},
animatedIn: {
animationDuration: `${ANIMATION_DURATION}ms`,
animationTimingFunction: 'cubic-bezier(0.215, 0.61, 0.355, 1)'
},
animatedOut: {
pointerEvents: 'none',
animationDuration: `${ANIMATION_DURATION}ms`,
animationTimingFunction: 'cubic-bezier(0.47, 0, 0.745, 0.715)'
},
fadeIn: {
opacity: 1,
animationKeyframes: {
'0%': { opacity: 0 },
'100%': { opacity: 1 }
}
},
fadeOut: {
opacity: 0,
animationKeyframes: {
'0%': { opacity: 1 },
'100%': { opacity: 0 }
}
},
slideIn: {
transform: 'translateY(0%)',
animationKeyframes: {
'0%': { transform: 'translateY(100%)' },
'100%': { transform: 'translateY(0%)' }
}
},
slideOut: {
transform: 'translateY(100%)',
animationKeyframes: {
'0%': { transform: 'translateY(0%)' },
'100%': { transform: 'translateY(100%)' }
}
},
hidden: {
opacity: 0
}
});
const animatedSlideInStyles = [
styles.container,
styles.animatedIn,
styles.slideIn
];
const animatedSlideOutStyles = [
styles.container,
styles.animatedOut,
styles.slideOut
];
const animatedFadeInStyles = [
styles.container,
styles.animatedIn,
styles.fadeIn
];
const animatedFadeOutStyles = [
styles.container,
styles.animatedOut,
styles.fadeOut
];
export default ModalAnimation;
@@ -0,0 +1,87 @@
/**
* Copyright (c) Nicolas Gallagher.
* 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
*/
import type { ViewProps } from '../View';
import * as React from 'react';
import View from '../View';
import StyleSheet from '../StyleSheet';
import canUseDOM from '../../modules/canUseDom';
export type ModalContentProps = {
...ViewProps,
active?: ?(boolean | (() => boolean)),
children?: any,
onRequestClose?: ?() => void,
transparent?: ?boolean
};
const ModalContent: React.AbstractComponent<
ModalContentProps,
React.ElementRef<typeof View>
> = React.forwardRef((props, forwardedRef) => {
const { active, children, onRequestClose, transparent, ...rest } = props;
React.useEffect(() => {
if (canUseDOM) {
const closeOnEscape = (e: KeyboardEvent) => {
if (active && e.key === 'Escape') {
e.stopPropagation();
if (onRequestClose) {
onRequestClose();
}
}
};
document.addEventListener('keyup', closeOnEscape, false);
return () => document.removeEventListener('keyup', closeOnEscape, false);
}
}, [active, onRequestClose]);
const style = React.useMemo(() => {
return [
styles.modal,
transparent ? styles.modalTransparent : styles.modalOpaque
];
}, [transparent]);
return (
<View
{...rest}
aria-modal={true}
ref={forwardedRef}
role={active ? 'dialog' : null}
style={style}
>
<View style={styles.container}>{children}</View>
</View>
);
});
const styles = StyleSheet.create({
modal: {
position: 'fixed',
top: 0,
right: 0,
bottom: 0,
left: 0
},
modalTransparent: {
backgroundColor: 'transparent'
},
modalOpaque: {
backgroundColor: 'white'
},
container: {
top: 0,
flex: 1
}
});
export default ModalContent;
@@ -0,0 +1,173 @@
/**
* Copyright (c) Nicolas Gallagher.
* 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
*/
import * as React from 'react';
import View from '../View';
import createElement from '../createElement';
import StyleSheet from '../StyleSheet';
import UIManager from '../UIManager';
import canUseDOM from '../../modules/canUseDom';
/**
* This Component is used to "wrap" the modal we're opening
* so that changing focus via tab will never leave the document.
*
* This allows us to properly trap the focus within a modal
* even if the modal is at the start or end of a document.
*/
const FocusBracket = () => {
return createElement('div', {
role: 'none',
tabIndex: 0,
style: styles.focusBracket
});
};
function attemptFocus(element: any) {
if (!canUseDOM) {
return false;
}
try {
element.focus();
} catch (e) {
// Do nothing
}
return document.activeElement === element;
}
function focusFirstDescendant(element: any) {
for (let i = 0; i < element.childNodes.length; i++) {
const child = element.childNodes[i];
if (attemptFocus(child) || focusFirstDescendant(child)) {
return true;
}
}
return false;
}
function focusLastDescendant(element: any) {
for (let i = element.childNodes.length - 1; i >= 0; i--) {
const child = element.childNodes[i];
if (attemptFocus(child) || focusLastDescendant(child)) {
return true;
}
}
return false;
}
export type ModalFocusTrapProps = {|
active?: boolean | (() => boolean),
children?: any
|};
const ModalFocusTrap = ({
active,
children
}: ModalFocusTrapProps): React.Node => {
const trapElementRef = React.useRef<?HTMLElement>();
const focusRef = React.useRef<{
trapFocusInProgress: boolean,
lastFocusedElement: ?HTMLElement
}>({
trapFocusInProgress: false,
lastFocusedElement: null
});
React.useEffect(() => {
if (canUseDOM) {
const trapFocus = () => {
// We should not trap focus if:
// - The modal hasn't fully initialized with an HTMLElement ref
// - Focus is already in the process of being trapped (e.g., we're refocusing)
// - isTrapActive prop being falsey tells us to do nothing
if (
trapElementRef.current == null ||
focusRef.current.trapFocusInProgress ||
!active
) {
return;
}
try {
focusRef.current.trapFocusInProgress = true;
if (
document.activeElement instanceof Node &&
!trapElementRef.current.contains(document.activeElement)
) {
// To handle keyboard focusing we can make an assumption here.
// If you're tabbing through the focusable elements, the previously
// active element will either be the first or the last.
// If the previously selected element is the "first" descendant
// and we're leaving it - this means that we should be looping
// around to the other side of the modal.
let hasFocused = focusFirstDescendant(trapElementRef.current);
if (
focusRef.current.lastFocusedElement === document.activeElement
) {
hasFocused = focusLastDescendant(trapElementRef.current);
}
// If we couldn't focus a new element then we need to focus onto the trap target
if (
!hasFocused &&
trapElementRef.current != null &&
document.activeElement
) {
UIManager.focus(trapElementRef.current);
}
}
} finally {
focusRef.current.trapFocusInProgress = false;
}
focusRef.current.lastFocusedElement = document.activeElement;
};
// Call the trapFocus callback at least once when this modal has been activated.
trapFocus();
document.addEventListener('focus', trapFocus, true);
return () => document.removeEventListener('focus', trapFocus, true);
}
}, [active]);
// To be fully compliant with WCAG we need to refocus element that triggered opening modal
// after closing it
React.useEffect(function () {
if (canUseDOM) {
const lastFocusedElementOutsideTrap = document.activeElement;
return function () {
if (
lastFocusedElementOutsideTrap &&
document.contains(lastFocusedElementOutsideTrap)
) {
UIManager.focus(lastFocusedElementOutsideTrap);
}
};
}
}, []);
return (
<>
<FocusBracket />
<View ref={trapElementRef}>{children}</View>
<FocusBracket />
</>
);
};
export default ModalFocusTrap;
const styles = StyleSheet.create({
focusBracket: {
outlineStyle: 'none'
}
});
@@ -0,0 +1,48 @@
/**
* Copyright (c) Nicolas Gallagher.
* 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
*/
import * as React from 'react';
import { createPortal } from 'react-dom';
import canUseDOM from '../../modules/canUseDom';
export type ModalPortalProps = {|
children: any
|};
function ModalPortal(props: ModalPortalProps): React.Node {
const { children } = props;
const elementRef = React.useRef(null);
if (canUseDOM && !elementRef.current) {
const element = document.createElement('div');
if (element && document.body) {
document.body.appendChild(element);
elementRef.current = element;
}
}
React.useEffect(() => {
if (canUseDOM) {
return () => {
if (document.body && elementRef.current) {
document.body.removeChild(elementRef.current);
elementRef.current = null;
}
};
}
}, []);
return elementRef.current && canUseDOM
? createPortal(children, elementRef.current)
: null;
}
export default ModalPortal;
+151
View File
@@ -0,0 +1,151 @@
/**
* Copyright (c) Nicolas Gallagher.
* 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
*/
'use client';
import type { ViewProps } from '../View';
import * as React from 'react';
import ModalPortal from './ModalPortal';
import ModalAnimation from './ModalAnimation';
import ModalContent from './ModalContent';
import ModalFocusTrap from './ModalFocusTrap';
export type ModalProps = {
...ViewProps,
animationType?: 'none' | 'slide' | 'fade',
children: any,
hardwareAccelerated?: ?boolean,
onDismiss?: ?() => mixed,
onOrientationChange?: ?(e: {|
orientation: 'portrait' | 'landscape'
|}) => void,
onRequestClose?: ?() => void,
onShow?: ?() => void,
presentationStyle?: ?(
| 'fullScreen'
| 'pageSheet'
| 'formSheet'
| 'overFullScreen'
),
statusBarTranslucent?: ?boolean,
supportedOrientations?: ?Array<
| 'portrait'
| 'portrait-upside-down'
| 'landscape'
| 'landscape-left'
| 'landscape-right'
>,
transparent?: ?boolean,
visible?: ?boolean
};
let uniqueModalIdentifier = 0;
const activeModalStack = [];
const activeModalListeners = {};
function notifyActiveModalListeners() {
if (activeModalStack.length === 0) {
return;
}
const activeModalId = activeModalStack[activeModalStack.length - 1];
activeModalStack.forEach((modalId) => {
if (modalId in activeModalListeners) {
activeModalListeners[modalId](modalId === activeModalId);
}
});
}
function removeActiveModal(modalId) {
if (modalId in activeModalListeners) {
// Before removing this listener we should probably tell it
// that it's no longer the active modal for sure.
activeModalListeners[modalId](false);
delete activeModalListeners[modalId];
}
const index = activeModalStack.indexOf(modalId);
if (index !== -1) {
activeModalStack.splice(index, 1);
notifyActiveModalListeners();
}
}
function addActiveModal(modalId, listener) {
removeActiveModal(modalId);
activeModalStack.push(modalId);
activeModalListeners[modalId] = listener;
notifyActiveModalListeners();
}
const Modal: React.AbstractComponent<
ModalProps,
React.ElementRef<typeof ModalContent>
> = React.forwardRef((props, forwardedRef) => {
const {
animationType,
children,
onDismiss,
onRequestClose,
onShow,
transparent,
visible = true,
...rest
} = props;
// Set a unique model identifier so we can correctly route
// dismissals and check the layering of modals.
const modalId = React.useMemo(() => uniqueModalIdentifier++, []);
const [isActive, setIsActive] = React.useState(false);
const onDismissCallback = React.useCallback(() => {
removeActiveModal(modalId);
if (onDismiss) {
onDismiss();
}
}, [modalId, onDismiss]);
const onShowCallback = React.useCallback(() => {
addActiveModal(modalId, setIsActive);
if (onShow) {
onShow();
}
}, [modalId, onShow]);
React.useEffect(() => {
return () => removeActiveModal(modalId);
}, [modalId]);
return (
<ModalPortal>
<ModalAnimation
animationType={animationType}
onDismiss={onDismissCallback}
onShow={onShowCallback}
visible={visible}
>
<ModalFocusTrap active={isActive}>
<ModalContent
{...rest}
active={isActive}
onRequestClose={onRequestClose}
ref={forwardedRef}
transparent={transparent}
>
{children}
</ModalContent>
</ModalFocusTrap>
</ModalAnimation>
</ModalPortal>
);
});
export default Modal;
@@ -0,0 +1,11 @@
/**
* 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
*/
import NativeEventEmitter from '../../vendor/react-native/EventEmitter/NativeEventEmitter';
export default NativeEventEmitter;
@@ -0,0 +1,17 @@
/**
* Copyright (c) Nicolas Gallagher.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @noflow
*/
import UIManager from '../UIManager';
// NativeModules shim
const NativeModules = {
UIManager
};
export default NativeModules;
@@ -0,0 +1,408 @@
/**
* 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
*/
/**
* PAN RESPONDER
*
* `PanResponder` uses the Responder System to reconcile several touches into
* a single gesture. It makes single-touch gestures resilient to extra touches,
* and can be used to recognize simple multi-touch gestures. For each handler,
* it provides a `gestureState` object alongside the ResponderEvent object.
*
* By default, `PanResponder` holds an `InteractionManager` handle to block
* long-running JS events from interrupting active gestures.
*
* A graphical explanation of the touch data flow:
*
* +----------------------------+ +--------------------------------+
* | ResponderTouchHistoryStore | |TouchHistoryMath |
* +----------------------------+ +----------+---------------------+
* |Global store of touchHistory| |Allocation-less math util |
* |including activeness, start | |on touch history (centroids |
* |position, prev/cur position.| |and multitouch movement etc) |
* | | | |
* +----^-----------------------+ +----^---------------------------+
* | |
* | (records relevant history |
* | of touches relevant for |
* | implementing higher level |
* | gestures) |
* | |
* +----+-----------------------+ +----|---------------------------+
* | ResponderEventPlugin | | | Your App/Component |
* +----------------------------+ +----|---------------------------+
* |Negotiates which view gets | Low level | | High level |
* |onResponderMove events. | events w/ | +-+-------+ events w/ |
* |Also records history into | touchHistory| | Pan | multitouch + |
* |ResponderTouchHistoryStore. +---------------->Responder+-----> accumulative|
* +----------------------------+ attached to | | | distance and |
* each event | +---------+ velocity. |
* | |
* | |
* +--------------------------------+
*/
'use strict';
import type { PressEvent } from '../../vendor/react-native/Types/CoreEventTypes';
import InteractionManager from '../InteractionManager';
import TouchHistoryMath from '../../vendor/react-native/TouchHistoryMath';
export type GestureState = {|
// ID of the gestureState; persisted as long as there's a pointer on screen
stateID: number,
// The latest screen coordinates of the gesture
x: number,
// The latest screen coordinates of the gesture
y: number,
// The screen coordinates of the responder grant
initialX: number,
// The screen coordinates of the responder grant
initialY: number,
// Accumulated distance of the gesture since it started
deltaX: number,
// Accumulated distance of the gesture since it started
deltaY: number,
// Current velocity of the gesture
velocityX: number,
// Current velocity of the gesture
velocityY: number,
// Number of touches currently on screen
numberActiveTouches: number,
_accountsForMovesUpTo: number
|};
type ActiveCallback = (
event: PressEvent,
gestureState: GestureState
) => boolean;
type PassiveCallback = (event: PressEvent, gestureState: GestureState) => void;
type PanResponderConfig = $ReadOnly<{|
// Negotiate for the responder
onMoveShouldSetResponder?: ?ActiveCallback,
onMoveShouldSetResponderCapture?: ?ActiveCallback,
onStartShouldSetResponder?: ?ActiveCallback,
onStartShouldSetResponderCapture?: ?ActiveCallback,
onPanTerminationRequest?: ?ActiveCallback,
// Gesture started
onPanGrant?: ?PassiveCallback,
// Gesture rejected
onPanReject?: ?PassiveCallback,
// A pointer touched the screen
onPanStart?: ?PassiveCallback,
// A pointer moved
onPanMove?: ?PassiveCallback,
// A pointer was removed from the screen
onPanEnd?: ?PassiveCallback,
// All pointers removed, gesture successful
onPanRelease?: ?PassiveCallback,
// Gesture cancelled
onPanTerminate?: ?PassiveCallback
|}>;
const {
currentCentroidX,
currentCentroidY,
currentCentroidXOfTouchesChangedAfter,
currentCentroidYOfTouchesChangedAfter,
previousCentroidXOfTouchesChangedAfter,
previousCentroidYOfTouchesChangedAfter
} = TouchHistoryMath;
const PanResponder = {
_initializeGestureState(gestureState: GestureState) {
gestureState.x = 0;
gestureState.y = 0;
gestureState.initialX = 0;
gestureState.initialY = 0;
gestureState.deltaX = 0;
gestureState.deltaY = 0;
gestureState.velocityX = 0;
gestureState.velocityY = 0;
gestureState.numberActiveTouches = 0;
// All `gestureState` accounts for timeStamps up until:
gestureState._accountsForMovesUpTo = 0;
},
/**
* Take all recently moved touches, calculate how the centroid has changed just for those
* recently moved touches, and append that change to an accumulator. This is
* to (at least) handle the case where the user is moving three fingers, and
* then one of the fingers stops but the other two continue.
*
* This is very different than taking all of the recently moved touches and
* storing their centroid as `dx/dy`. For correctness, we must *accumulate
* changes* in the centroid of recently moved touches.
*
* There is also some nuance with how we handle multiple moved touches in a
* single event. Multiple touches generate two 'move' events, each of
* them triggering `onResponderMove`. But with the way `PanResponder` works,
* all of the gesture inference is performed on the first dispatch, since it
* looks at all of the touches. Therefore, `PanResponder` does not call
* `onResponderMove` passed the first dispatch. This diverges from the
* typical responder callback pattern (without using `PanResponder`), but
* avoids more dispatches than necessary.
*
* When moving two touches in opposite directions, the cumulative
* distance is zero in each dimension. When two touches move in parallel five
* pixels in the same direction, the cumulative distance is five, not ten. If
* two touches start, one moves five in a direction, then stops and the other
* touch moves fives in the same direction, the cumulative distance is ten.
*
* This logic requires a kind of processing of time "clusters" of touch events
* so that two touch moves that essentially occur in parallel but move every
* other frame respectively, are considered part of the same movement.
*
* x/y: If a move event has been observed, `(x, y)` is the centroid of the most
* recently moved "cluster" of active touches.
* deltaX/deltaY: Cumulative touch distance. Accounts for touch moves that are
* clustered together in time, moving the same direction. Only valid when
* currently responder (otherwise, it only represents the drag distance below
* the threshold).
*/
_updateGestureStateOnMove(
gestureState: GestureState,
touchHistory: $PropertyType<PressEvent, 'touchHistory'>
) {
const movedAfter = gestureState._accountsForMovesUpTo;
const prevX = previousCentroidXOfTouchesChangedAfter(
touchHistory,
movedAfter
);
const prevY = previousCentroidYOfTouchesChangedAfter(
touchHistory,
movedAfter
);
const prevDeltaX = gestureState.deltaX;
const prevDeltaY = gestureState.deltaY;
const x = currentCentroidXOfTouchesChangedAfter(touchHistory, movedAfter);
const y = currentCentroidYOfTouchesChangedAfter(touchHistory, movedAfter);
const deltaX = prevDeltaX + (x - prevX);
const deltaY = prevDeltaY + (y - prevY);
// TODO: This must be filtered intelligently.
const dt =
touchHistory.mostRecentTimeStamp - gestureState._accountsForMovesUpTo;
gestureState.deltaX = deltaX;
gestureState.deltaY = deltaY;
gestureState.numberActiveTouches = touchHistory.numberActiveTouches;
gestureState.velocityX = (deltaX - prevDeltaX) / dt;
gestureState.velocityY = (deltaY - prevDeltaY) / dt;
gestureState.x = x;
gestureState.y = y;
gestureState._accountsForMovesUpTo = touchHistory.mostRecentTimeStamp;
},
/**
* Enhanced versions of all of the responder callbacks that provide not only
* the `ResponderEvent`, but also the `PanResponder` gesture state.
*
* In general, for events that have capture equivalents, we update the
* gestureState once in the capture phase and can use it in the bubble phase
* as well.
*/
create(config: PanResponderConfig): {|
getInteractionHandle: () => ?number,
panHandlers: {|
onMoveShouldSetResponder: (event: PressEvent) => boolean,
onMoveShouldSetResponderCapture: (event: PressEvent) => boolean,
onResponderEnd: (event: PressEvent) => void,
onResponderGrant: (event: PressEvent) => void,
onResponderMove: (event: PressEvent) => void,
onResponderReject: (event: PressEvent) => void,
onResponderRelease: (event: PressEvent) => void,
onResponderStart: (event: PressEvent) => void,
onResponderTerminate: (event: PressEvent) => void,
onResponderTerminationRequest: (event: PressEvent) => boolean,
onStartShouldSetResponder: (event: PressEvent) => boolean,
onStartShouldSetResponderCapture: (event: PressEvent) => boolean
|}
|} {
const interactionState = {
handle: (null: ?number)
};
const gestureState: GestureState = {
// Useful for debugging
stateID: Math.random(),
x: 0,
y: 0,
initialX: 0,
initialY: 0,
deltaX: 0,
deltaY: 0,
velocityX: 0,
velocityY: 0,
numberActiveTouches: 0,
_accountsForMovesUpTo: 0
};
const {
onStartShouldSetResponder,
onStartShouldSetResponderCapture,
onMoveShouldSetResponder,
onMoveShouldSetResponderCapture,
onPanGrant,
onPanStart,
onPanMove,
onPanEnd,
onPanRelease,
onPanReject,
onPanTerminate,
onPanTerminationRequest
} = config;
const panHandlers = {
onStartShouldSetResponder(event: PressEvent): boolean {
return onStartShouldSetResponder != null
? onStartShouldSetResponder(event, gestureState)
: false;
},
onMoveShouldSetResponder(event: PressEvent): boolean {
return onMoveShouldSetResponder != null
? onMoveShouldSetResponder(event, gestureState)
: false;
},
onStartShouldSetResponderCapture(event: PressEvent): boolean {
// TODO: Actually, we should reinitialize the state any time
// touches.length increases from 0 active to > 0 active.
if (event.nativeEvent.touches.length === 1) {
PanResponder._initializeGestureState(gestureState);
}
gestureState.numberActiveTouches =
event.touchHistory.numberActiveTouches;
return onStartShouldSetResponderCapture != null
? onStartShouldSetResponderCapture(event, gestureState)
: false;
},
onMoveShouldSetResponderCapture(event: PressEvent): boolean {
const touchHistory = event.touchHistory;
// Responder system incorrectly dispatches should* to current responder
// Filter out any touch moves past the first one - we would have
// already processed multi-touch geometry during the first event.
// NOTE: commented out because new responder system should get it right.
//if (gestureState._accountsForMovesUpTo === touchHistory.mostRecentTimeStamp) {
// return false;
//}
PanResponder._updateGestureStateOnMove(gestureState, touchHistory);
return onMoveShouldSetResponderCapture != null
? onMoveShouldSetResponderCapture(event, gestureState)
: false;
},
onResponderGrant(event: PressEvent): void {
if (!interactionState.handle) {
interactionState.handle =
InteractionManager.createInteractionHandle();
}
gestureState.initialX = currentCentroidX(event.touchHistory);
gestureState.initialY = currentCentroidY(event.touchHistory);
gestureState.deltaX = 0;
gestureState.deltaY = 0;
if (onPanGrant != null) {
onPanGrant(event, gestureState);
}
},
onResponderReject(event: PressEvent): void {
clearInteractionHandle(
interactionState,
onPanReject,
event,
gestureState
);
},
onResponderStart(event: PressEvent): void {
const { numberActiveTouches } = event.touchHistory;
gestureState.numberActiveTouches = numberActiveTouches;
if (onPanStart != null) {
onPanStart(event, gestureState);
}
},
onResponderMove(event: PressEvent): void {
const touchHistory = event.touchHistory;
// Guard against the dispatch of two touch moves when there are two
// simultaneously changed touches.
if (
gestureState._accountsForMovesUpTo ===
touchHistory.mostRecentTimeStamp
) {
return;
}
// Filter out any touch moves past the first one - we would have
// already processed multi-touch geometry during the first event.
PanResponder._updateGestureStateOnMove(gestureState, touchHistory);
if (onPanMove != null) {
onPanMove(event, gestureState);
}
},
onResponderEnd(event: PressEvent): void {
const { numberActiveTouches } = event.touchHistory;
gestureState.numberActiveTouches = numberActiveTouches;
clearInteractionHandle(interactionState, onPanEnd, event, gestureState);
},
onResponderRelease(event: PressEvent): void {
clearInteractionHandle(
interactionState,
onPanRelease,
event,
gestureState
);
PanResponder._initializeGestureState(gestureState);
},
onResponderTerminate(event: PressEvent): void {
clearInteractionHandle(
interactionState,
onPanTerminate,
event,
gestureState
);
PanResponder._initializeGestureState(gestureState);
},
onResponderTerminationRequest(event: PressEvent): boolean {
return onPanTerminationRequest != null
? onPanTerminationRequest(event, gestureState)
: true;
}
};
return {
panHandlers,
getInteractionHandle(): ?number {
return interactionState.handle;
}
};
}
};
function clearInteractionHandle(
interactionState: { handle: ?number },
callback: ?(ActiveCallback | PassiveCallback),
event: PressEvent,
gestureState: GestureState
) {
if (interactionState.handle) {
InteractionManager.clearInteractionHandle(interactionState.handle);
interactionState.handle = null;
}
if (callback) {
callback(event, gestureState);
}
}
export default PanResponder;
@@ -0,0 +1,4 @@
// @flow strict
import PanResponder from '../../vendor/react-native/PanResponder';
export default PanResponder;
@@ -0,0 +1,26 @@
/**
* Copyright (c) Nicolas Gallagher.
* 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.
*
* @noflow
*/
import type { ColorValue } from '../../types';
import createElement from '../createElement';
type Props = {
color?: ColorValue,
label: string,
testID?: string,
value?: number | string
};
export default function PickerItem(props: Props) {
const { color, label, testID, value } = props;
const style = { color };
return createElement('option', { children: label, style, testID, value });
}
@@ -0,0 +1,95 @@
/**
* Copyright (c) Nicolas Gallagher.
* 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
*/
'use client';
import type { PlatformMethods } from '../../types';
import type { ViewProps } from '../View';
import * as React from 'react';
import createElement from '../createElement';
import useMergeRefs from '../../modules/useMergeRefs';
import usePlatformMethods from '../../modules/usePlatformMethods';
import PickerItem from './PickerItem';
import StyleSheet from '../StyleSheet';
type PickerProps = {
...ViewProps,
children?: typeof PickerItem | Array<typeof PickerItem>,
enabled?: boolean,
onValueChange?: (number | string, number) => void,
selectedValue?: number | string,
style?: any,
/* compat */
itemStyle?: any,
mode?: string,
prompt?: string
};
const Picker: React.AbstractComponent<
PickerProps,
HTMLElement & PlatformMethods
> = React.forwardRef((props, forwardedRef) => {
const {
children,
enabled,
onValueChange,
selectedValue,
style,
testID,
/* eslint-disable */
itemStyle,
mode,
prompt,
/* eslint-enable */
...other
} = props;
const hostRef = React.useRef(null);
function handleChange(e: Object) {
const { selectedIndex, value } = e.target;
if (onValueChange) {
onValueChange(value, selectedIndex);
}
}
// $FlowFixMe
const supportedProps: any = {
children,
disabled: enabled === false ? true : undefined,
onChange: handleChange,
style: [styles.initial, style],
testID,
value: selectedValue,
...other
};
const platformMethodsRef = usePlatformMethods(supportedProps);
const setRef = useMergeRefs(hostRef, platformMethodsRef, forwardedRef);
supportedProps.ref = setRef;
return createElement('select', supportedProps);
});
// $FlowFixMe
Picker.Item = PickerItem;
const styles = StyleSheet.create({
initial: {
fontFamily: 'System',
fontSize: 'inherit',
margin: 0
}
});
export default Picker;
@@ -0,0 +1,49 @@
/**
* Copyright (c) Nicolas Gallagher.
* 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
*/
import Dimensions from '../Dimensions';
/**
* PixelRatio gives access to the device pixel density.
*/
export default class PixelRatio {
/**
* Returns the device pixel density.
*/
static get(): number {
return Dimensions.get('window').scale;
}
/**
* No equivalent for Web
*/
static getFontScale(): number {
return Dimensions.get('window').fontScale || PixelRatio.get();
}
/**
* Converts a layout size (dp) to pixel size (px).
* Guaranteed to return an integer number.
*/
static getPixelSizeForLayoutSize(layoutSize: number): number {
return Math.round(layoutSize * PixelRatio.get());
}
/**
* Rounds a layout size (dp) to the nearest layout size that corresponds to
* an integer number of pixels. For example, on a device with a PixelRatio
* of 3, `PixelRatio.roundToNearestPixel(8.4) = 8.33`, which corresponds to
* exactly (8.33 * 3) = 25 pixels.
*/
static roundToNearestPixel(layoutSize: number): number {
const ratio = PixelRatio.get();
return Math.round(layoutSize * ratio) / ratio;
}
}
@@ -0,0 +1,25 @@
/**
* Copyright (c) Nicolas Gallagher.
* 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
*/
const Platform = {
OS: 'web',
select: (obj: Object): any => ('web' in obj ? obj.web : obj.default),
get isTesting(): boolean {
if (process.env.NODE_ENV === 'test') {
return true;
}
return false;
},
get Version(): string {
return '0.0.0';
}
};
export default Platform;
@@ -0,0 +1,242 @@
/**
* 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
*/
'use client';
import type { HoverEventsConfig } from '../../modules/useHover';
import type { PressResponderConfig } from '../../modules/usePressEvents/PressResponder';
import type { ViewProps } from '../View';
import * as React from 'react';
import { forwardRef, memo, useMemo, useState, useRef } from 'react';
import useMergeRefs from '../../modules/useMergeRefs';
import useHover from '../../modules/useHover';
import usePressEvents from '../../modules/usePressEvents';
import StyleSheet from '../StyleSheet';
import View from '../View';
export type StateCallbackType = $ReadOnly<{|
focused: boolean,
hovered: boolean,
pressed: boolean
|}>;
type ViewStyleProp = $PropertyType<ViewProps, 'style'>;
type Props = {
...ViewProps,
children: React.Node | ((state: StateCallbackType) => React.Node),
// Duration (in milliseconds) from `onPressIn` before `onLongPress` is called.
delayLongPress?: ?number,
// Duration (in milliseconds) from `onPressStart` is called after pointerdown
delayPressIn?: ?number,
// Duration (in milliseconds) from `onPressEnd` is called after pointerup.
delayPressOut?: ?number,
// Whether the press behavior is disabled.
disabled?: ?boolean,
// Called when the view is hovered
onHoverIn?: $PropertyType<HoverEventsConfig, 'onHoverStart'>,
// Called when the view is no longer hovered
onHoverOut?: $PropertyType<HoverEventsConfig, 'onHoverEnd'>,
// Called when this view's layout changes
onLayout?: $PropertyType<ViewProps, 'onLayout'>,
// Called when a long-tap gesture is detected.
onLongPress?: $PropertyType<PressResponderConfig, 'onLongPress'>,
// Called when a single tap gesture is detected.
onPress?: $PropertyType<PressResponderConfig, 'onPress'>,
// Called when a touch is engaged, before `onPress`.
onPressIn?: $PropertyType<PressResponderConfig, 'onPressStart'>,
// Called when a touch is moving, after `onPressIn`.
onPressMove?: $PropertyType<PressResponderConfig, 'onPressMove'>,
// Called when a touch is released, before `onPress`.
onPressOut?: $PropertyType<PressResponderConfig, 'onPressEnd'>,
style?: ViewStyleProp | ((state: StateCallbackType) => ViewStyleProp),
/**
* Used only for documentation or testing (e.g. snapshot testing).
*/
testOnly_hovered?: ?boolean,
testOnly_pressed?: ?boolean
};
/**
* Component used to build display components that should respond to whether the
* component is currently pressed or not.
*/
function Pressable(props: Props, forwardedRef): React.Node {
const {
children,
delayLongPress,
delayPressIn,
delayPressOut,
disabled,
onBlur,
onContextMenu,
onFocus,
onHoverIn,
onHoverOut,
onKeyDown,
onLongPress,
onPress,
onPressMove,
onPressIn,
onPressOut,
style,
tabIndex,
testOnly_hovered,
testOnly_pressed,
...rest
} = props;
const [hovered, setHovered] = useForceableState(testOnly_hovered === true);
const [focused, setFocused] = useForceableState(false);
const [pressed, setPressed] = useForceableState(testOnly_pressed === true);
const hostRef = useRef(null);
const setRef = useMergeRefs(forwardedRef, hostRef);
const pressConfig = useMemo(
() => ({
delayLongPress,
delayPressStart: delayPressIn,
delayPressEnd: delayPressOut,
disabled,
onLongPress,
onPress,
onPressChange: setPressed,
onPressStart: onPressIn,
onPressMove,
onPressEnd: onPressOut
}),
[
delayLongPress,
delayPressIn,
delayPressOut,
disabled,
onLongPress,
onPress,
onPressIn,
onPressMove,
onPressOut,
setPressed
]
);
const pressEventHandlers = usePressEvents(hostRef, pressConfig);
const { onContextMenu: onContextMenuPress, onKeyDown: onKeyDownPress } =
pressEventHandlers;
useHover(hostRef, {
contain: true,
disabled,
onHoverChange: setHovered,
onHoverStart: onHoverIn,
onHoverEnd: onHoverOut
});
const interactionState = { hovered, focused, pressed };
const blurHandler = React.useCallback(
(e) => {
if (e.nativeEvent.target === hostRef.current) {
setFocused(false);
if (onBlur != null) {
onBlur(e);
}
}
},
[hostRef, setFocused, onBlur]
);
const focusHandler = React.useCallback(
(e) => {
if (e.nativeEvent.target === hostRef.current) {
setFocused(true);
if (onFocus != null) {
onFocus(e);
}
}
},
[hostRef, setFocused, onFocus]
);
const contextMenuHandler = React.useCallback(
(e) => {
if (onContextMenuPress != null) {
onContextMenuPress(e);
}
if (onContextMenu != null) {
onContextMenu(e);
}
},
[onContextMenu, onContextMenuPress]
);
const keyDownHandler = React.useCallback(
(e) => {
if (onKeyDownPress != null) {
onKeyDownPress(e);
}
if (onKeyDown != null) {
onKeyDown(e);
}
},
[onKeyDown, onKeyDownPress]
);
let _tabIndex;
if (tabIndex !== undefined) {
_tabIndex = tabIndex;
} else {
_tabIndex = disabled ? -1 : 0;
}
return (
<View
{...rest}
{...pressEventHandlers}
aria-disabled={disabled}
onBlur={blurHandler}
onContextMenu={contextMenuHandler}
onFocus={focusHandler}
onKeyDown={keyDownHandler}
ref={setRef}
style={[
disabled ? styles.disabled : styles.active,
typeof style === 'function' ? style(interactionState) : style
]}
tabIndex={_tabIndex}
>
{typeof children === 'function' ? children(interactionState) : children}
</View>
);
}
function useForceableState(forced: boolean): [boolean, (boolean) => void] {
const [bool, setBool] = useState(false);
return [bool || forced, setBool];
}
const styles = StyleSheet.create({
active: {
cursor: 'pointer',
touchAction: 'manipulation'
},
disabled: {
pointerEvents: 'box-none'
}
});
const MemoedPressable = memo(forwardRef(Pressable));
MemoedPressable.displayName = 'Pressable';
export default (MemoedPressable: React.AbstractComponent<
Props,
React.ElementRef<typeof View>
>);
@@ -0,0 +1,92 @@
/**
* Copyright (c) Nicolas Gallagher.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
'use client';
import type { ColorValue } from '../../types';
import type { ViewProps } from '../View';
import * as React from 'react';
import StyleSheet from '../StyleSheet';
import View from '../View';
type ProgressBarProps = {
...ViewProps,
color?: ColorValue,
indeterminate?: boolean,
progress?: number,
trackColor?: ColorValue
};
const ProgressBar: React.AbstractComponent<
ProgressBarProps,
React.ElementRef<typeof View>
> = React.forwardRef((props, ref) => {
const {
color = '#1976D2',
indeterminate = false,
progress = 0,
trackColor = 'transparent',
style,
...other
} = props;
const percentageProgress = progress * 100;
const width = indeterminate ? '25%' : `${percentageProgress}%`;
return (
<View
{...other}
aria-valuemax={100}
aria-valuemin={0}
aria-valuenow={indeterminate ? null : percentageProgress}
ref={ref}
role="progressbar"
style={[styles.track, style, { backgroundColor: trackColor }]}
>
<View
style={[
{ backgroundColor: color, width },
styles.progress,
indeterminate && styles.animation
]}
/>
</View>
);
});
ProgressBar.displayName = 'ProgressBar';
const styles = StyleSheet.create({
track: {
forcedColorAdjust: 'none',
height: 5,
overflow: 'hidden',
userSelect: 'none',
zIndex: 0
},
progress: {
forcedColorAdjust: 'none',
height: '100%',
zIndex: -1
},
animation: {
animationDuration: '1s',
animationKeyframes: [
{
'0%': { transform: 'translateX(-100%)' },
'100%': { transform: 'translateX(400%)' }
}
],
animationTimingFunction: 'linear',
animationIterationCount: 'infinite'
}
});
export default ProgressBar;
@@ -0,0 +1,52 @@
/**
* Copyright (c) Nicolas Gallagher.
* 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
*/
import type { ColorValue } from '../../types';
import type { ViewProps } from '../View';
import type { Node } from 'React';
import View from '../View';
import React from 'react';
type RefreshControlProps = {
...ViewProps,
colors?: Array<ColorValue>,
enabled?: boolean,
onRefresh?: () => void,
progressBackgroundColor?: ColorValue,
progressViewOffset?: number,
refreshing: boolean,
size?: 0 | 1,
tintColor?: ColorValue,
title?: string,
titleColor?: ColorValue
};
function RefreshControl(props: RefreshControlProps): Node {
const {
/* eslint-disable */
colors,
enabled,
onRefresh,
progressBackgroundColor,
progressViewOffset,
refreshing,
size,
tintColor,
title,
titleColor,
/* eslint-enable */
...rest
} = props;
return <View {...rest} />;
}
export default RefreshControl;
@@ -0,0 +1,49 @@
/**
* Copyright (c) Nicolas Gallagher.
* 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
*/
import type { ViewProps } from '../View';
import * as React from 'react';
import StyleSheet from '../StyleSheet';
import View from '../View';
import canUseDOM from '../../modules/canUseDom';
const cssFunction: 'constant' | 'env' = (function () {
if (
canUseDOM &&
window.CSS &&
window.CSS.supports &&
window.CSS.supports('top: constant(safe-area-inset-top)')
) {
return 'constant';
}
return 'env';
})();
const SafeAreaView: React.AbstractComponent<
ViewProps,
React.ElementRef<typeof View>
> = React.forwardRef((props, ref) => {
const { style, ...rest } = props;
return <View {...rest} ref={ref} style={[styles.root, style]} />;
});
SafeAreaView.displayName = 'SafeAreaView';
const styles = StyleSheet.create({
root: {
paddingTop: `${cssFunction}(safe-area-inset-top)`,
paddingRight: `${cssFunction}(safe-area-inset-right)`,
paddingBottom: `${cssFunction}(safe-area-inset-bottom)`,
paddingLeft: `${cssFunction}(safe-area-inset-left)`
}
});
export default SafeAreaView;
@@ -0,0 +1,183 @@
/**
* Copyright (c) Nicolas Gallagher.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import type { ViewProps } from '../View';
import * as React from 'react';
import StyleSheet from '../StyleSheet';
import View from '../View';
import useMergeRefs from '../../modules/useMergeRefs';
type Props = {
...ViewProps,
onMomentumScrollBegin?: (e: any) => void,
onMomentumScrollEnd?: (e: any) => void,
onScroll?: (e: any) => void,
onScrollBeginDrag?: (e: any) => void,
onScrollEndDrag?: (e: any) => void,
onTouchMove?: (e: any) => void,
onWheel?: (e: any) => void,
scrollEnabled?: boolean,
scrollEventThrottle?: number,
showsHorizontalScrollIndicator?: boolean,
showsVerticalScrollIndicator?: boolean
};
function normalizeScrollEvent(e) {
return {
nativeEvent: {
contentOffset: {
get x() {
return e.target.scrollLeft;
},
get y() {
return e.target.scrollTop;
}
},
contentSize: {
get height() {
return e.target.scrollHeight;
},
get width() {
return e.target.scrollWidth;
}
},
layoutMeasurement: {
get height() {
return e.target.offsetHeight;
},
get width() {
return e.target.offsetWidth;
}
}
},
timeStamp: Date.now()
};
}
function shouldEmitScrollEvent(lastTick: number, eventThrottle: number) {
const timeSinceLastTick = Date.now() - lastTick;
return eventThrottle > 0 && timeSinceLastTick >= eventThrottle;
}
/**
* Encapsulates the Web-specific scroll throttling and disabling logic
*/
const ScrollViewBase: React.AbstractComponent<
Props,
React.ElementRef<typeof View>
> = React.forwardRef((props, forwardedRef) => {
const {
onScroll,
onTouchMove,
onWheel,
scrollEnabled = true,
scrollEventThrottle = 0,
showsHorizontalScrollIndicator,
showsVerticalScrollIndicator,
style,
...rest
} = props;
const scrollState = React.useRef({ isScrolling: false, scrollLastTick: 0 });
const scrollTimeout = React.useRef(null);
const scrollRef = React.useRef(null);
function createPreventableScrollHandler(handler: Function) {
return (e: Object) => {
if (scrollEnabled) {
if (handler) {
handler(e);
}
}
};
}
function handleScroll(e: Object) {
e.stopPropagation();
if (e.target === scrollRef.current) {
e.persist();
// A scroll happened, so the scroll resets the scrollend timeout.
if (scrollTimeout.current != null) {
clearTimeout(scrollTimeout.current);
}
scrollTimeout.current = setTimeout(() => {
handleScrollEnd(e);
}, 100);
if (scrollState.current.isScrolling) {
// Scroll last tick may have changed, check if we need to notify
if (
shouldEmitScrollEvent(
scrollState.current.scrollLastTick,
scrollEventThrottle
)
) {
handleScrollTick(e);
}
} else {
// Weren't scrolling, so we must have just started
handleScrollStart(e);
}
}
}
function handleScrollStart(e: Object) {
scrollState.current.isScrolling = true;
handleScrollTick(e);
}
function handleScrollTick(e: Object) {
scrollState.current.scrollLastTick = Date.now();
if (onScroll) {
onScroll(normalizeScrollEvent(e));
}
}
function handleScrollEnd(e: Object) {
scrollState.current.isScrolling = false;
if (onScroll) {
onScroll(normalizeScrollEvent(e));
}
}
const hideScrollbar =
showsHorizontalScrollIndicator === false ||
showsVerticalScrollIndicator === false;
return (
<View
{...rest}
onScroll={handleScroll}
onTouchMove={createPreventableScrollHandler(onTouchMove)}
onWheel={createPreventableScrollHandler(onWheel)}
ref={useMergeRefs(scrollRef, forwardedRef)}
style={[
style,
!scrollEnabled && styles.scrollDisabled,
hideScrollbar && styles.hideScrollbar
]}
/>
);
});
// Chrome doesn't support e.preventDefault in this case; touch-action must be
// used to disable scrolling.
// https://developers.google.com/web/updates/2017/01/scrolling-intervention
const styles = StyleSheet.create({
scrollDisabled: {
overflowX: 'hidden',
overflowY: 'hidden',
touchAction: 'none'
},
hideScrollbar: {
scrollbarWidth: 'none'
}
});
export default ScrollViewBase;
@@ -0,0 +1,784 @@
/**
* Copyright (c) Nicolas Gallagher.
* 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.
*
* @noflow
*/
'use client';
import type { ViewProps, ViewStyle } from '../View/types';
import Dimensions from '../Dimensions';
import dismissKeyboard from '../../modules/dismissKeyboard';
import invariant from 'fbjs/lib/invariant';
import mergeRefs from '../../modules/mergeRefs';
import Platform from '../Platform';
import ScrollViewBase from './ScrollViewBase';
import StyleSheet from '../StyleSheet';
import TextInputState from '../../modules/TextInputState';
import UIManager from '../UIManager';
import View from '../View';
import React from 'react';
import warning from 'fbjs/lib/warning';
type ScrollViewProps = {
...ViewProps,
centerContent?: boolean,
contentContainerStyle?: ViewStyle,
horizontal?: boolean,
keyboardDismissMode?: 'none' | 'interactive' | 'on-drag',
onContentSizeChange?: (e: any) => void,
onScroll?: (e: any) => void,
pagingEnabled?: boolean,
refreshControl?: any,
scrollEnabled?: boolean,
scrollEventThrottle?: number,
stickyHeaderIndices?: Array<number>
};
type Event = Object;
const emptyObject = {};
const IS_ANIMATING_TOUCH_START_THRESHOLD_MS = 16;
class ScrollView extends React.Component<ScrollViewProps> {
_scrollNodeRef: any = null;
_innerViewRef: any = null;
/**
* ------------------------------------------------------
* START SCROLLRESPONDER
* ------------------------------------------------------
*/
isTouching: boolean = false;
lastMomentumScrollBeginTime: number = 0;
lastMomentumScrollEndTime: number = 0;
// Reset to false every time becomes responder. This is used to:
// - Determine if the scroll view has been scrolled and therefore should
// refuse to give up its responder lock.
// - Determine if releasing should dismiss the keyboard when we are in
// tap-to-dismiss mode (!this.props.keyboardShouldPersistTaps).
observedScrollSinceBecomingResponder: boolean = false;
becameResponderWhileAnimating: boolean = false;
/**
* Invoke this from an `onScroll` event.
*/
scrollResponderHandleScrollShouldSetResponder: boolean = () => {
return this.isTouching;
};
/**
* 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 {
return false;
}
/**
* 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: boolean = (e: Event) => {
// First see if we want to eat taps while the keyboard is up
// var currentlyFocusedTextInput = TextInputState.currentlyFocusedField();
// if (!this.props.keyboardShouldPersistTaps &&
// currentlyFocusedTextInput != null &&
// e.target !== currentlyFocusedTextInput) {
// return true;
// }
return this.scrollResponderIsAnimating();
};
/**
* 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() {
warning(false, "ScrollView doesn't take rejection well - scrolls anyway");
}
/**
* 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 = () => {
return !this.observedScrollSinceBecomingResponder;
};
/**
* Invoke this from an `onTouchEnd` event.
*
* @param {SyntheticEvent} e Event.
*/
scrollResponderHandleTouchEnd = (e: Event) => {
const nativeEvent = e.nativeEvent;
this.isTouching = nativeEvent.touches.length !== 0;
this.props.onTouchEnd && this.props.onTouchEnd(e);
};
/**
* Invoke this from an `onResponderRelease` event.
*/
scrollResponderHandleResponderRelease = (e: Event) => {
this.props.onResponderRelease && this.props.onResponderRelease(e);
// By default scroll views will unfocus a textField
// if another touch occurs outside of it
const currentlyFocusedTextInput = TextInputState.currentlyFocusedField();
if (
!this.props.keyboardShouldPersistTaps &&
currentlyFocusedTextInput != null &&
e.target !== currentlyFocusedTextInput &&
!this.observedScrollSinceBecomingResponder &&
!this.becameResponderWhileAnimating
) {
this.props.onScrollResponderKeyboardDismissed &&
this.props.onScrollResponderKeyboardDismissed(e);
TextInputState.blurTextInput(currentlyFocusedTextInput);
}
};
scrollResponderHandleScroll = (e: Event) => {
this.observedScrollSinceBecomingResponder = true;
this.props.onScroll && this.props.onScroll(e);
};
/**
* Invoke this from an `onResponderGrant` event.
*/
scrollResponderHandleResponderGrant = (e: Event) => {
this.observedScrollSinceBecomingResponder = false;
this.props.onResponderGrant && this.props.onResponderGrant(e);
this.becameResponderWhileAnimating = this.scrollResponderIsAnimating();
};
/**
* 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: Event) => {
this.props.onScrollBeginDrag && this.props.onScrollBeginDrag(e);
};
/**
* Invoke this from an `onScrollEndDrag` event.
*/
scrollResponderHandleScrollEndDrag = (e: Event) => {
this.props.onScrollEndDrag && this.props.onScrollEndDrag(e);
};
/**
* Invoke this from an `onMomentumScrollBegin` event.
*/
scrollResponderHandleMomentumScrollBegin = (e: Event) => {
this.lastMomentumScrollBeginTime = Date.now();
this.props.onMomentumScrollBegin && this.props.onMomentumScrollBegin(e);
};
/**
* Invoke this from an `onMomentumScrollEnd` event.
*/
scrollResponderHandleMomentumScrollEnd = (e: Event) => {
this.lastMomentumScrollEndTime = Date.now();
this.props.onMomentumScrollEnd && this.props.onMomentumScrollEnd(e);
};
/**
* 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 {SyntheticEvent} e Touch Start event.
*/
scrollResponderHandleTouchStart = (e: Event) => {
this.isTouching = true;
this.props.onTouchStart && this.props.onTouchStart(e);
};
/**
* 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 {SyntheticEvent} e Touch Start event.
*/
scrollResponderHandleTouchMove = (e: Event) => {
this.props.onTouchMove && this.props.onTouchMove(e);
};
/**
* 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 = () => {
const now = Date.now();
const timeSinceLastMomentumScrollEnd = now - this.lastMomentumScrollEndTime;
const isAnimating =
timeSinceLastMomentumScrollEnd < IS_ANIMATING_TOUCH_START_THRESHOLD_MS ||
this.lastMomentumScrollEndTime < this.lastMomentumScrollBeginTime;
return isAnimating;
};
/**
* 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 as alternative to the options object.
* This is deprecated due to ambiguity (y before x), and SHOULD NOT BE USED.
*/
scrollResponderScrollTo = (
x?: number | { x?: number, y?: number, animated?: boolean },
y?: number,
animated?: boolean
) => {
if (typeof x === 'number') {
console.warn(
'`scrollResponderScrollTo(x, y, animated)` is deprecated. Use `scrollResponderScrollTo({x: 5, y: 5, animated: true})` instead.'
);
} else {
({ x, y, animated } = x || emptyObject);
}
const node = this.getScrollableNode();
const left = x || 0;
const top = y || 0;
if (node != null) {
if (typeof node.scroll === 'function') {
node.scroll({ top, left, behavior: !animated ? 'auto' : 'smooth' });
} else {
node.scrollLeft = left;
node.scrollTop = top;
}
}
};
/**
* 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
},
animated?: boolean // deprecated, put this inside the rect argument instead
) => {
if (Platform.OS !== 'ios') {
invariant('zoomToRect is not implemented');
}
};
/**
* Displays the scroll indicators momentarily.
*/
scrollResponderFlashScrollIndicators() {}
/**
* 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 {any} nodeHandle The TextInput node handle
* @param {number} additionalOffset The scroll view's top "contentInset".
* Default is 0.
* @param {bool} 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
) => {
this.additionalScrollOffset = additionalOffset || 0;
this.preventNegativeScrollOffset = !!preventNegativeScrollOffset;
UIManager.measureLayout(
nodeHandle,
this.getInnerViewNode(),
this.scrollResponderTextInputFocusError,
this.scrollResponderInputMeasureAndScrollToKeyboard
);
};
/**
* 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 {number} left Position of input w.r.t. table view.
* @param {number} top Position of input w.r.t. table view.
* @param {number} width Width of the text input.
* @param {number} height Height of the text input.
*/
scrollResponderInputMeasureAndScrollToKeyboard = (
left: number,
top: number,
width: number,
height: number
) => {
let keyboardScreenY = Dimensions.get('window').height;
if (this.keyboardWillOpenTo) {
keyboardScreenY = this.keyboardWillOpenTo.endCoordinates.screenY;
}
let scrollOffsetY =
top - keyboardScreenY + height + this.additionalScrollOffset;
// By default, this can scroll with negative offset, pulling the content
// down so that the target component's bottom meets the keyboard's top.
// If requested otherwise, cap the offset at 0 minimum to avoid content
// shifting down.
if (this.preventNegativeScrollOffset) {
scrollOffsetY = Math.max(0, scrollOffsetY);
}
this.scrollResponderScrollTo({ x: 0, y: scrollOffsetY, animated: true });
this.additionalOffset = 0;
this.preventNegativeScrollOffset = false;
};
scrollResponderTextInputFocusError(e: Event) {
console.error('Error measuring text field: ', e);
}
/**
* 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: Event) => {
this.keyboardWillOpenTo = e;
this.props.onKeyboardWillShow && this.props.onKeyboardWillShow(e);
};
scrollResponderKeyboardWillHide = (e: Event) => {
this.keyboardWillOpenTo = null;
this.props.onKeyboardWillHide && this.props.onKeyboardWillHide(e);
};
scrollResponderKeyboardDidShow = (e: Event) => {
// TODO(7693961): The event for DidShow is not available on iOS yet.
// Use the one from WillShow and do not assign.
if (e) {
this.keyboardWillOpenTo = e;
}
this.props.onKeyboardDidShow && this.props.onKeyboardDidShow(e);
};
scrollResponderKeyboardDidHide = (e: Event) => {
this.keyboardWillOpenTo = null;
this.props.onKeyboardDidHide && this.props.onKeyboardDidHide(e);
};
/**
* ------------------------------------------------------
* END SCROLLRESPONDER
* ------------------------------------------------------
*/
flashScrollIndicators = () => {
this.scrollResponderFlashScrollIndicators();
};
/**
* 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: ScrollView = () => {
return this;
};
getScrollableNode = () => {
return this._scrollNodeRef;
};
getInnerViewRef = () => {
return this._innerViewRef;
};
getInnerViewNode = () => {
return this._innerViewRef;
};
getNativeScrollRef = () => {
return this._scrollNodeRef;
};
/**
* 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 as alternative to the options object.
* This is deprecated due to ambiguity (y before x), and SHOULD NOT BE USED.
*/
scrollTo = (
y?: number | { x?: number, y?: number, animated?: boolean },
x?: number,
animated?: boolean
) => {
if (typeof y === 'number') {
console.warn(
'`scrollTo(y, x, animated)` is deprecated. Use `scrollTo({x: 5, y: 5, animated: true})` instead.'
);
} else {
({ x, y, animated } = y || emptyObject);
}
this.scrollResponderScrollTo({
x: x || 0,
y: y || 0,
animated: animated !== false
});
};
/**
* If this is a vertical ScrollView scrolls to the bottom.
* If this is a horizontal ScrollView scrolls to the right.
*
* Use `scrollToEnd({ animated: true })` for smooth animated scrolling,
* `scrollToEnd({ animated: false })` for immediate scrolling.
* If no options are passed, `animated` defaults to true.
*/
scrollToEnd = (options?: { animated?: boolean }) => {
// Default to true
const animated = (options && options.animated) !== false;
const { horizontal } = this.props;
const scrollResponderNode = this.getScrollableNode();
const x = horizontal ? scrollResponderNode.scrollWidth : 0;
const y = horizontal ? 0 : scrollResponderNode.scrollHeight;
this.scrollResponderScrollTo({ x, y, animated });
};
render() {
const {
contentContainerStyle,
horizontal,
onContentSizeChange,
refreshControl,
stickyHeaderIndices,
pagingEnabled,
/* eslint-disable */
forwardedRef,
keyboardDismissMode,
onScroll,
centerContent,
/* eslint-enable */
...other
} = this.props;
if (process.env.NODE_ENV !== 'production' && this.props.style) {
const style = StyleSheet.flatten(this.props.style);
const childLayoutProps = ['alignItems', 'justifyContent'].filter(
(prop) => style && style[prop] !== undefined
);
invariant(
childLayoutProps.length === 0,
`ScrollView child layout (${JSON.stringify(childLayoutProps)}) ` +
'must be applied through the contentContainerStyle prop.'
);
}
let contentSizeChangeProps = {};
if (onContentSizeChange) {
contentSizeChangeProps = {
onLayout: this._handleContentOnLayout
};
}
const hasStickyHeaderIndices =
!horizontal && Array.isArray(stickyHeaderIndices);
const children =
hasStickyHeaderIndices || pagingEnabled
? React.Children.map(this.props.children, (child, i) => {
const isSticky =
hasStickyHeaderIndices && stickyHeaderIndices.indexOf(i) > -1;
if (child != null && (isSticky || pagingEnabled)) {
return (
<View
style={[
isSticky && styles.stickyHeader,
pagingEnabled && styles.pagingEnabledChild
]}
>
{child}
</View>
);
} else {
return child;
}
})
: this.props.children;
const contentContainer = (
<View
{...contentSizeChangeProps}
children={children}
collapsable={false}
ref={this._setInnerViewRef}
style={[
horizontal && styles.contentContainerHorizontal,
centerContent && styles.contentContainerCenterContent,
contentContainerStyle
]}
/>
);
const baseStyle = horizontal ? styles.baseHorizontal : styles.baseVertical;
const pagingEnabledStyle = horizontal
? styles.pagingEnabledHorizontal
: styles.pagingEnabledVertical;
const props = {
...other,
style: [baseStyle, pagingEnabled && pagingEnabledStyle, this.props.style],
onTouchStart: this.scrollResponderHandleTouchStart,
onTouchMove: this.scrollResponderHandleTouchMove,
onTouchEnd: this.scrollResponderHandleTouchEnd,
onScrollBeginDrag: this.scrollResponderHandleScrollBeginDrag,
onScrollEndDrag: this.scrollResponderHandleScrollEndDrag,
onMomentumScrollBegin: this.scrollResponderHandleMomentumScrollBegin,
onMomentumScrollEnd: this.scrollResponderHandleMomentumScrollEnd,
onStartShouldSetResponder:
this.scrollResponderHandleStartShouldSetResponder,
onStartShouldSetResponderCapture:
this.scrollResponderHandleStartShouldSetResponderCapture,
onScrollShouldSetResponder:
this.scrollResponderHandleScrollShouldSetResponder,
onScroll: this._handleScroll,
onResponderGrant: this.scrollResponderHandleResponderGrant,
onResponderTerminationRequest:
this.scrollResponderHandleTerminationRequest,
onResponderTerminate: this.scrollResponderHandleTerminate,
onResponderRelease: this.scrollResponderHandleResponderRelease,
onResponderReject: this.scrollResponderHandleResponderReject
};
const ScrollViewClass = ScrollViewBase;
invariant(
ScrollViewClass !== undefined,
'ScrollViewClass must not be undefined'
);
const scrollView = (
<ScrollViewClass {...props} ref={this._setScrollNodeRef}>
{contentContainer}
</ScrollViewClass>
);
if (refreshControl) {
return React.cloneElement(
refreshControl,
{ style: props.style },
scrollView
);
}
return scrollView;
}
_handleContentOnLayout = (e: Object) => {
const { width, height } = e.nativeEvent.layout;
this.props.onContentSizeChange(width, height);
};
_handleScroll = (e: Object) => {
if (process.env.NODE_ENV !== 'production') {
if (this.props.onScroll && this.props.scrollEventThrottle == null) {
console.log(
'You specified `onScroll` on a <ScrollView> but not ' +
'`scrollEventThrottle`. You will only receive one event. ' +
'Using `16` you get all the events but be aware that it may ' +
"cause frame drops, use a bigger number if you don't need as " +
'much precision.'
);
}
}
if (this.props.keyboardDismissMode === 'on-drag') {
dismissKeyboard();
}
this.scrollResponderHandleScroll(e);
};
_setInnerViewRef = (node) => {
this._innerViewRef = node;
};
_setScrollNodeRef = (node) => {
this._scrollNodeRef = node;
// ScrollView needs to add more methods to the hostNode in addition to those
// added by `usePlatformMethods`. This is temporarily until an API like
// `ScrollView.scrollTo(hostNode, { x, y })` is added to React Native.
if (node != null) {
node.getScrollResponder = this.getScrollResponder;
node.getInnerViewNode = this.getInnerViewNode;
node.getInnerViewRef = this.getInnerViewRef;
node.getNativeScrollRef = this.getNativeScrollRef;
node.getScrollableNode = this.getScrollableNode;
node.scrollTo = this.scrollTo;
node.scrollToEnd = this.scrollToEnd;
node.flashScrollIndicators = this.flashScrollIndicators;
node.scrollResponderZoomTo = this.scrollResponderZoomTo;
node.scrollResponderScrollNativeHandleToKeyboard =
this.scrollResponderScrollNativeHandleToKeyboard;
}
const ref = mergeRefs(this.props.forwardedRef);
ref(node);
};
}
const commonStyle = {
flexGrow: 1,
flexShrink: 1,
// Enable hardware compositing in modern browsers.
// Creates a new layer with its own backing surface that can significantly
// improve scroll performance.
transform: 'translateZ(0)',
// iOS native scrolling
WebkitOverflowScrolling: 'touch'
};
const styles = StyleSheet.create({
baseVertical: {
...commonStyle,
flexDirection: 'column',
overflowX: 'hidden',
overflowY: 'auto'
},
baseHorizontal: {
...commonStyle,
flexDirection: 'row',
overflowX: 'auto',
overflowY: 'hidden'
},
contentContainerHorizontal: {
flexDirection: 'row'
},
contentContainerCenterContent: {
justifyContent: 'center',
flexGrow: 1
},
stickyHeader: {
position: 'sticky',
top: 0,
zIndex: 10
},
pagingEnabledHorizontal: {
scrollSnapType: 'x mandatory'
},
pagingEnabledVertical: {
scrollSnapType: 'y mandatory'
},
pagingEnabledChild: {
scrollSnapAlign: 'start'
}
});
const ForwardedScrollView: React.AbstractComponent<
React.ElementConfig<typeof ScrollView>,
React.ElementRef<typeof ScrollView>
> = React.forwardRef((props, forwardedRef) => {
return <ScrollView {...props} forwardedRef={forwardedRef} />;
});
ForwardedScrollView.displayName = 'ScrollView';
export default ForwardedScrollView;
@@ -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
*/
'use client';
import SectionList from '../../vendor/react-native/SectionList';
export default SectionList;
@@ -0,0 +1,65 @@
/**
* Copyright (c) Nicolas Gallagher.
* 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
*/
import invariant from 'fbjs/lib/invariant';
type Content =
| { title?: string, message?: string, url: string }
| { title?: string, message: string, url?: string };
class Share {
static share(content: Content, options: Object = {}): Promise<Object> {
invariant(
typeof content === 'object' && content !== null,
'Content to share must be a valid object'
);
invariant(
typeof content.url === 'string' || typeof content.message === 'string',
'At least one of URL and message is required'
);
invariant(
typeof options === 'object' && options !== null,
'Options must be a valid object'
);
invariant(
!content.title || typeof content.title === 'string',
'Invalid title: title should be a string.'
);
if (window.navigator.share !== undefined) {
return window.navigator.share({
title: content.title,
text: content.message,
url: content.url
});
} else {
return Promise.reject(
new Error('Share is not supported in this browser')
);
}
}
/**
* The content was successfully shared.
*/
static get sharedAction(): string {
return 'sharedAction';
}
/**
* The dialog has been dismissed.
* @platform ios
*/
static get dismissedAction(): string {
return 'dismissedAction';
}
}
export default Share;
@@ -0,0 +1,22 @@
/**
* Copyright (c) Nicolas Gallagher.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
const emptyFunction = () => {};
function StatusBar(): null {
return null;
}
StatusBar.setBackgroundColor = emptyFunction;
StatusBar.setBarStyle = emptyFunction;
StatusBar.setHidden = emptyFunction;
StatusBar.setNetworkActivityIndicatorVisible = emptyFunction;
StatusBar.setTranslucent = emptyFunction;
export default StatusBar;
@@ -0,0 +1,214 @@
/**
* Copyright (c) Nicolas Gallagher.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import normalizeValueWithProperty from './normalizeValueWithProperty';
import canUseDOM from '../../../modules/canUseDom';
type Style = { [key: string]: any };
/**
* The browser implements the CSS cascade, where the order of properties is a
* factor in determining which styles to paint. React Native is different. It
* gives giving precedence to the more specific style property. For example,
* the value of `paddingTop` takes precedence over that of `padding`.
*
* This module creates mutally exclusive style declarations by expanding all of
* React Native's supported shortform properties (e.g. `padding`) to their
* longfrom equivalents.
*/
const emptyObject = {};
const supportsCSS3TextDecoration =
!canUseDOM ||
(window.CSS != null &&
window.CSS.supports != null &&
(window.CSS.supports('text-decoration-line', 'none') ||
window.CSS.supports('-webkit-text-decoration-line', 'none')));
const MONOSPACE_FONT_STACK = 'monospace,monospace';
const SYSTEM_FONT_STACK =
'-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif';
const STYLE_SHORT_FORM_EXPANSIONS = {
borderColor: [
'borderTopColor',
'borderRightColor',
'borderBottomColor',
'borderLeftColor'
],
borderBlockColor: ['borderTopColor', 'borderBottomColor'],
borderInlineColor: ['borderRightColor', 'borderLeftColor'],
borderRadius: [
'borderTopLeftRadius',
'borderTopRightRadius',
'borderBottomRightRadius',
'borderBottomLeftRadius'
],
borderStyle: [
'borderTopStyle',
'borderRightStyle',
'borderBottomStyle',
'borderLeftStyle'
],
borderBlockStyle: ['borderTopStyle', 'borderBottomStyle'],
borderInlineStyle: ['borderRightStyle', 'borderLeftStyle'],
borderWidth: [
'borderTopWidth',
'borderRightWidth',
'borderBottomWidth',
'borderLeftWidth'
],
borderBlockWidth: ['borderTopWidth', 'borderBottomWidth'],
borderInlineWidth: ['borderRightWidth', 'borderLeftWidth'],
insetBlock: ['top', 'bottom'],
insetInline: ['left', 'right'],
marginBlock: ['marginTop', 'marginBottom'],
marginInline: ['marginRight', 'marginLeft'],
paddingBlock: ['paddingTop', 'paddingBottom'],
paddingInline: ['paddingRight', 'paddingLeft'],
overflow: ['overflowX', 'overflowY'],
overscrollBehavior: ['overscrollBehaviorX', 'overscrollBehaviorY'],
borderBlockStartColor: ['borderTopColor'],
borderBlockStartStyle: ['borderTopStyle'],
borderBlockStartWidth: ['borderTopWidth'],
borderBlockEndColor: ['borderBottomColor'],
borderBlockEndStyle: ['borderBottomStyle'],
borderBlockEndWidth: ['borderBottomWidth'],
//borderInlineStartColor: ['borderLeftColor'],
//borderInlineStartStyle: ['borderLeftStyle'],
//borderInlineStartWidth: ['borderLeftWidth'],
//borderInlineEndColor: ['borderRightColor'],
//borderInlineEndStyle: ['borderRightStyle'],
//borderInlineEndWidth: ['borderRightWidth'],
borderEndStartRadius: ['borderBottomLeftRadius'],
borderEndEndRadius: ['borderBottomRightRadius'],
borderStartStartRadius: ['borderTopLeftRadius'],
borderStartEndRadius: ['borderTopRightRadius'],
insetBlockEnd: ['bottom'],
insetBlockStart: ['top'],
//insetInlineEnd: ['right'],
//insetInlineStart: ['left'],
marginBlockStart: ['marginTop'],
marginBlockEnd: ['marginBottom'],
//marginInlineStart: ['marginLeft'],
//marginInlineEnd: ['marginRight'],
paddingBlockStart: ['paddingTop'],
paddingBlockEnd: ['paddingBottom']
//paddingInlineStart: ['marginLeft'],
//paddingInlineEnd: ['marginRight'],
};
/**
* Reducer
*/
const createReactDOMStyle = (style: Style, isInline?: boolean): Style => {
if (!style) {
return emptyObject;
}
const resolvedStyle = {};
for (const prop in style) {
const value = style[prop];
if (
// Ignore everything with a null value
value == null
) {
continue;
}
if (prop === 'backgroundClip') {
// TODO: remove once this issue is fixed
// https://github.com/rofrischmann/inline-style-prefixer/issues/159
if (value === 'text') {
resolvedStyle.backgroundClip = value;
resolvedStyle.WebkitBackgroundClip = value;
}
} else if (prop === 'flex') {
if (value === -1) {
resolvedStyle.flexGrow = 0;
resolvedStyle.flexShrink = 1;
resolvedStyle.flexBasis = 'auto';
} else {
resolvedStyle.flex = value;
}
} else if (prop === 'font') {
resolvedStyle[prop] = value.replace('System', SYSTEM_FONT_STACK);
} else if (prop === 'fontFamily') {
if (value.indexOf('System') > -1) {
const stack = value.split(/,\s*/);
stack[stack.indexOf('System')] = SYSTEM_FONT_STACK;
resolvedStyle[prop] = stack.join(',');
} else if (value === 'monospace') {
resolvedStyle[prop] = MONOSPACE_FONT_STACK;
} else {
resolvedStyle[prop] = value;
}
} else if (prop === 'textDecorationLine') {
// use 'text-decoration' for browsers that only support CSS2
// text-decoration (e.g., IE, Edge)
if (!supportsCSS3TextDecoration) {
resolvedStyle.textDecoration = value;
} else {
resolvedStyle.textDecorationLine = value;
}
} else if (prop === 'writingDirection') {
resolvedStyle.direction = value;
} else {
const value = normalizeValueWithProperty(style[prop], prop);
const longFormProperties = STYLE_SHORT_FORM_EXPANSIONS[prop];
if (isInline && prop === 'inset') {
if (style.insetInline == null) {
resolvedStyle.left = value;
resolvedStyle.right = value;
}
if (style.insetBlock == null) {
resolvedStyle.top = value;
resolvedStyle.bottom = value;
}
} else if (isInline && prop === 'margin') {
if (style.marginInline == null) {
resolvedStyle.marginLeft = value;
resolvedStyle.marginRight = value;
}
if (style.marginBlock == null) {
resolvedStyle.marginTop = value;
resolvedStyle.marginBottom = value;
}
} else if (isInline && prop === 'padding') {
if (style.paddingInline == null) {
resolvedStyle.paddingLeft = value;
resolvedStyle.paddingRight = value;
}
if (style.paddingBlock == null) {
resolvedStyle.paddingTop = value;
resolvedStyle.paddingBottom = value;
}
} else if (longFormProperties) {
longFormProperties.forEach((longForm, i) => {
// The value of any longform property in the original styles takes
// precedence over the shortform's value.
if (style[longForm] == null) {
resolvedStyle[longForm] = value;
}
});
} else {
resolvedStyle[prop] = value;
}
}
}
return resolvedStyle;
};
export default createReactDOMStyle;
@@ -0,0 +1,67 @@
/* eslint-disable */
/**
* JS Implementation of MurmurHash2
*
* @author <a href="mailto:gary.court@gmail.com">Gary Court</a>
* @see http://github.com/garycourt/murmurhash-js
* @author <a href="mailto:aappleby@gmail.com">Austin Appleby</a>
* @see http://sites.google.com/site/murmurhash/
*
* @param {string} str ASCII only
* @param {number} seed Positive integer only
* @return {number} 32-bit positive integer hash
*
* @flow
*/
function murmurhash2_32_gc(str, seed) {
var l = str.length,
h = seed ^ l,
i = 0,
k;
while (l >= 4) {
k =
(str.charCodeAt(i) & 0xff) |
((str.charCodeAt(++i) & 0xff) << 8) |
((str.charCodeAt(++i) & 0xff) << 16) |
((str.charCodeAt(++i) & 0xff) << 24);
k =
(k & 0xffff) * 0x5bd1e995 + ((((k >>> 16) * 0x5bd1e995) & 0xffff) << 16);
k ^= k >>> 24;
k =
(k & 0xffff) * 0x5bd1e995 + ((((k >>> 16) * 0x5bd1e995) & 0xffff) << 16);
h =
((h & 0xffff) * 0x5bd1e995 +
((((h >>> 16) * 0x5bd1e995) & 0xffff) << 16)) ^
k;
l -= 4;
++i;
}
switch (l) {
case 3:
h ^= (str.charCodeAt(i + 2) & 0xff) << 16;
case 2:
h ^= (str.charCodeAt(i + 1) & 0xff) << 8;
case 1:
h ^= str.charCodeAt(i) & 0xff;
h =
(h & 0xffff) * 0x5bd1e995 +
((((h >>> 16) * 0x5bd1e995) & 0xffff) << 16);
}
h ^= h >>> 13;
h = (h & 0xffff) * 0x5bd1e995 + ((((h >>> 16) * 0x5bd1e995) & 0xffff) << 16);
h ^= h >>> 15;
return h >>> 0;
}
const hash = (str: string): string => murmurhash2_32_gc(str, 1).toString(36);
export default hash;
@@ -0,0 +1,27 @@
/**
* Copyright (c) Nicolas Gallagher.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict
*/
const uppercasePattern = /[A-Z]/g;
const msPattern = /^ms-/;
const cache = {};
function toHyphenLower(match) {
return '-' + match.toLowerCase();
}
function hyphenateStyleName(name: string): string {
if (name in cache) {
return cache[name];
}
const hName = name.replace(uppercasePattern, toHyphenLower);
return (cache[name] = msPattern.test(hName) ? '-' + hName : hName);
}
export default hyphenateStyleName;
@@ -0,0 +1,518 @@
/**
* Copyright (c) Nicolas Gallagher.
*
* 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
*/
import createReactDOMStyle from './createReactDOMStyle';
import hash from './hash';
import hyphenateStyleName from './hyphenateStyleName';
import normalizeValueWithProperty from './normalizeValueWithProperty';
import prefixStyles from '../../../modules/prefixStyles';
type Value = Object | Array<any> | string | number;
type Style = { [key: string]: Value };
type Rule = string;
type Rules = Array<Rule>;
type RulesData = [Rules, number];
type CompiledStyle = {
$$css: boolean,
$$css$localize?: boolean,
[key: string]: string | Array<string>
};
type CompilerOutput = [CompiledStyle, Array<RulesData>];
const cache = new Map();
const emptyObject = {};
const classicGroup = 1;
const atomicGroup = 3;
const customGroup: { [key: string]: number } = {
borderColor: 2,
borderRadius: 2,
borderStyle: 2,
borderWidth: 2,
display: 2,
flex: 2,
inset: 2,
margin: 2,
overflow: 2,
overscrollBehavior: 2,
padding: 2,
insetBlock: 2.1,
insetInline: 2.1,
marginInline: 2.1,
marginBlock: 2.1,
paddingInline: 2.1,
paddingBlock: 2.1,
borderBlockStartColor: 2.2,
borderBlockStartStyle: 2.2,
borderBlockStartWidth: 2.2,
borderBlockEndColor: 2.2,
borderBlockEndStyle: 2.2,
borderBlockEndWidth: 2.2,
borderInlineStartColor: 2.2,
borderInlineStartStyle: 2.2,
borderInlineStartWidth: 2.2,
borderInlineEndColor: 2.2,
borderInlineEndStyle: 2.2,
borderInlineEndWidth: 2.2,
borderEndStartRadius: 2.2,
borderEndEndRadius: 2.2,
borderStartStartRadius: 2.2,
borderStartEndRadius: 2.2,
insetBlockEnd: 2.2,
insetBlockStart: 2.2,
insetInlineEnd: 2.2,
insetInlineStart: 2.2,
marginBlockStart: 2.2,
marginBlockEnd: 2.2,
marginInlineStart: 2.2,
marginInlineEnd: 2.2,
paddingBlockStart: 2.2,
paddingBlockEnd: 2.2,
paddingInlineStart: 2.2,
paddingInlineEnd: 2.2
};
const borderTopLeftRadius = 'borderTopLeftRadius';
const borderTopRightRadius = 'borderTopRightRadius';
const borderBottomLeftRadius = 'borderBottomLeftRadius';
const borderBottomRightRadius = 'borderBottomRightRadius';
const borderLeftColor = 'borderLeftColor';
const borderLeftStyle = 'borderLeftStyle';
const borderLeftWidth = 'borderLeftWidth';
const borderRightColor = 'borderRightColor';
const borderRightStyle = 'borderRightStyle';
const borderRightWidth = 'borderRightWidth';
const right = 'right';
const marginLeft = 'marginLeft';
const marginRight = 'marginRight';
const paddingLeft = 'paddingLeft';
const paddingRight = 'paddingRight';
const left = 'left';
// Map of LTR property names to their BiDi equivalent.
const PROPERTIES_FLIP: { [key: string]: string } = {
[borderTopLeftRadius]: borderTopRightRadius,
[borderTopRightRadius]: borderTopLeftRadius,
[borderBottomLeftRadius]: borderBottomRightRadius,
[borderBottomRightRadius]: borderBottomLeftRadius,
[borderLeftColor]: borderRightColor,
[borderLeftStyle]: borderRightStyle,
[borderLeftWidth]: borderRightWidth,
[borderRightColor]: borderLeftColor,
[borderRightStyle]: borderLeftStyle,
[borderRightWidth]: borderLeftWidth,
[left]: right,
[marginLeft]: marginRight,
[marginRight]: marginLeft,
[paddingLeft]: paddingRight,
[paddingRight]: paddingLeft,
[right]: left
};
// Map of I18N property names to their LTR equivalent.
const PROPERTIES_I18N: { [key: string]: string } = {
borderStartStartRadius: borderTopLeftRadius,
borderStartEndRadius: borderTopRightRadius,
borderEndStartRadius: borderBottomLeftRadius,
borderEndEndRadius: borderBottomRightRadius,
borderInlineStartColor: borderLeftColor,
borderInlineStartStyle: borderLeftStyle,
borderInlineStartWidth: borderLeftWidth,
borderInlineEndColor: borderRightColor,
borderInlineEndStyle: borderRightStyle,
borderInlineEndWidth: borderRightWidth,
insetInlineEnd: right,
insetInlineStart: left,
marginInlineStart: marginLeft,
marginInlineEnd: marginRight,
paddingInlineStart: paddingLeft,
paddingInlineEnd: paddingRight
};
const PROPERTIES_VALUE = ['clear', 'float', 'textAlign'];
export function atomic(style: Style): CompilerOutput {
const compiledStyle: CompiledStyle = { $$css: true };
const compiledRules = [];
function atomicCompile(srcProp, prop, value) {
const valueString = stringifyValueWithProperty(value, prop);
const cacheKey = prop + valueString;
const cachedResult = cache.get(cacheKey);
let identifier;
if (cachedResult != null) {
identifier = cachedResult[0];
compiledRules.push(cachedResult[1]);
} else {
const v = srcProp !== prop ? cacheKey : valueString;
identifier = createIdentifier('r', srcProp, v);
const order = customGroup[srcProp] || atomicGroup;
const rules = createAtomicRules(identifier, prop, value);
const orderedRules = [rules, order];
compiledRules.push(orderedRules);
cache.set(cacheKey, [identifier, orderedRules]);
}
return identifier;
}
Object.keys(style)
.sort()
.forEach((srcProp) => {
const value = style[srcProp];
if (value != null) {
let localizeableValue;
// BiDi flip values
if (PROPERTIES_VALUE.indexOf(srcProp) > -1) {
const left = atomicCompile(srcProp, srcProp, 'left');
const right = atomicCompile(srcProp, srcProp, 'right');
if (value === 'start') {
localizeableValue = [left, right];
} else if (value === 'end') {
localizeableValue = [right, left];
}
}
// BiDi flip properties
const propPolyfill = PROPERTIES_I18N[srcProp];
if (propPolyfill != null) {
const ltr = atomicCompile(srcProp, propPolyfill, value);
const rtl = atomicCompile(
srcProp,
PROPERTIES_FLIP[propPolyfill],
value
);
localizeableValue = [ltr, rtl];
}
// BiDi flip transitionProperty value
if (srcProp === 'transitionProperty') {
const values = Array.isArray(value) ? value : [value];
const polyfillIndices = [];
for (let i = 0; i < values.length; i++) {
const val = values[i];
if (typeof val === 'string' && PROPERTIES_I18N[val] != null) {
polyfillIndices.push(i);
}
}
if (polyfillIndices.length > 0) {
const ltrPolyfillValues = [...values];
const rtlPolyfillValues = [...values];
polyfillIndices.forEach((i) => {
const ltrVal = ltrPolyfillValues[i];
if (typeof ltrVal === 'string') {
const ltrPolyfill = PROPERTIES_I18N[ltrVal];
const rtlPolyfill = PROPERTIES_FLIP[ltrPolyfill];
ltrPolyfillValues[i] = ltrPolyfill;
rtlPolyfillValues[i] = rtlPolyfill;
const ltr = atomicCompile(srcProp, srcProp, ltrPolyfillValues);
const rtl = atomicCompile(srcProp, srcProp, rtlPolyfillValues);
localizeableValue = [ltr, rtl];
}
});
}
}
if (localizeableValue == null) {
localizeableValue = atomicCompile(srcProp, srcProp, value);
} else {
compiledStyle['$$css$localize'] = true;
}
compiledStyle[srcProp] = localizeableValue;
}
});
return [compiledStyle, compiledRules];
}
/**
* Compile simple style object to classic CSS rules.
* No support for 'placeholderTextColor', 'scrollbarWidth', or 'pointerEvents'.
*/
export function classic(style: Style, name: string): CompilerOutput {
const compiledStyle = { $$css: true };
const compiledRules = [];
const { animationKeyframes, ...rest } = style;
const identifier = createIdentifier('css', name, JSON.stringify(style));
const selector = `.${identifier}`;
let animationName;
if (animationKeyframes != null) {
const [animationNames, keyframesRules] =
processKeyframesValue(animationKeyframes);
animationName = animationNames.join(',');
compiledRules.push(...keyframesRules);
}
const block = createDeclarationBlock({ ...rest, animationName });
compiledRules.push(`${selector}${block}`);
compiledStyle[identifier] = identifier;
return [compiledStyle, [[compiledRules, classicGroup]]];
}
/**
* Compile simple style object to inline DOM styles.
* No support for 'animationKeyframes', 'placeholderTextColor', 'scrollbarWidth', or 'pointerEvents'.
*/
export function inline(
originalStyle: Style,
isRTL?: boolean
): { [key: string]: mixed } {
const style = originalStyle || emptyObject;
const frozenProps = {};
const nextStyle = {};
for (const originalProp in style) {
const originalValue = style[originalProp];
let prop = originalProp;
let value = originalValue;
if (
!Object.prototype.hasOwnProperty.call(style, originalProp) ||
originalValue == null
) {
continue;
}
// BiDi flip values
if (PROPERTIES_VALUE.indexOf(originalProp) > -1) {
if (originalValue === 'start') {
value = isRTL ? 'right' : 'left';
} else if (originalValue === 'end') {
value = isRTL ? 'left' : 'right';
}
}
// BiDi flip properties
const propPolyfill = PROPERTIES_I18N[originalProp];
if (propPolyfill != null) {
prop = isRTL ? PROPERTIES_FLIP[propPolyfill] : propPolyfill;
}
// BiDi flip transitionProperty value
if (originalProp === 'transitionProperty') {
// $FlowFixMe
const originalValues = Array.isArray(originalValue)
? originalValue
: [originalValue];
originalValues.forEach((val, i) => {
if (typeof val === 'string') {
const valuePolyfill = PROPERTIES_I18N[val];
if (valuePolyfill != null) {
originalValues[i] = isRTL
? PROPERTIES_FLIP[valuePolyfill]
: valuePolyfill;
value = originalValues.join(' ');
}
}
});
}
// Create finalized style
if (!frozenProps[prop]) {
nextStyle[prop] = value;
}
if (prop === originalProp) {
frozenProps[prop] = true;
}
// if (PROPERTIES_I18N.hasOwnProperty(originalProp)) {
// frozenProps[prop] = true;
//}
}
return createReactDOMStyle(nextStyle, true);
}
/**
* Create a value string that normalizes different input values with a common
* output.
*/
export function stringifyValueWithProperty(
value: Value,
property: ?string
): string {
// e.g., 0 => '0px', 'black' => 'rgba(0,0,0,1)'
const normalizedValue = normalizeValueWithProperty(value, property);
return typeof normalizedValue !== 'string'
? JSON.stringify(normalizedValue || '')
: normalizedValue;
}
/**
* Create the Atomic CSS rules needed for a given StyleSheet rule.
* Translates StyleSheet declarations to CSS.
*/
function createAtomicRules(identifier: string, property, value): Rules {
const rules = [];
const selector = `.${identifier}`;
// Handle non-standard properties and object values that require multiple
// CSS rules to be created.
switch (property) {
case 'animationKeyframes': {
const [animationNames, keyframesRules] = processKeyframesValue(value);
const block = createDeclarationBlock({
animationName: animationNames.join(',')
});
rules.push(`${selector}${block}`, ...keyframesRules);
break;
}
// Equivalent to using '::placeholder'
case 'placeholderTextColor': {
const block = createDeclarationBlock({ color: value, opacity: 1 });
rules.push(
`${selector}::-webkit-input-placeholder${block}`,
`${selector}::-moz-placeholder${block}`,
`${selector}:-ms-input-placeholder${block}`,
`${selector}::placeholder${block}`
);
break;
}
// Polyfill for additional 'pointer-events' values
// See d13f78622b233a0afc0c7a200c0a0792c8ca9e58
// See https://reactnative.dev/docs/view#pointerevents
case 'pointerEvents': {
let finalValue = value;
if (value === 'auto') {
finalValue = 'auto!important';
} else if (value === 'none') {
finalValue = 'none!important';
const block = createDeclarationBlock({ pointerEvents: 'none' });
rules.push(`${selector}>* ${block}`);
} else if (value === 'box-none') {
finalValue = 'none!important';
const block = createDeclarationBlock({ pointerEvents: 'auto' });
rules.push(`${selector}>* ${block}`);
} else if (value === 'box-only') {
finalValue = 'auto!important';
const block = createDeclarationBlock({ pointerEvents: 'none' });
rules.push(`${selector}>* ${block}`);
}
const block = createDeclarationBlock({ pointerEvents: finalValue });
rules.push(`${selector}${block}`);
break;
}
// Polyfill for draft spec
// https://drafts.csswg.org/css-scrollbars-1/
case 'scrollbarWidth': {
if (value === 'none') {
rules.push(`${selector}::-webkit-scrollbar{display:none}`);
}
const block = createDeclarationBlock({ scrollbarWidth: value });
rules.push(`${selector}${block}`);
break;
}
default: {
const block = createDeclarationBlock({ [property]: value });
rules.push(`${selector}${block}`);
break;
}
}
return rules;
}
/**
* Creates a CSS declaration block from a StyleSheet object.
*/
function createDeclarationBlock(style: Style): string {
const domStyle = prefixStyles(createReactDOMStyle(style));
const declarationsString = Object.keys(domStyle)
.map((property) => {
const value = domStyle[property];
const prop = hyphenateStyleName(property);
// The prefixer may return an array of values:
// { display: [ '-webkit-flex', 'flex' ] }
// to represent "fallback" declarations
// { display: -webkit-flex; display: flex; }
if (Array.isArray(value)) {
return value.map((v) => `${prop}:${v}`).join(';');
} else {
return `${prop}:${value}`;
}
})
// Once properties are hyphenated, this will put the vendor
// prefixed and short-form properties first in the list.
.sort()
.join(';');
return `{${declarationsString};}`;
}
/**
* An identifier is associated with a unique set of styles.
*/
function createIdentifier(prefix: string, name: string, key: string): string {
const hashedString = hash(name + key);
return process.env.NODE_ENV !== 'production'
? `${prefix}-${name}-${hashedString}`
: `${prefix}-${hashedString}`;
}
/**
* Create individual CSS keyframes rules.
*/
function createKeyframes(keyframes: Object): [string, Rules] {
const prefixes = ['-webkit-', ''];
const identifier = createIdentifier(
'r',
'animation',
JSON.stringify(keyframes)
);
const steps =
'{' +
Object.keys(keyframes)
.map((stepName) => {
const rule = keyframes[stepName];
const block = createDeclarationBlock(rule);
return `${stepName}${block}`;
})
.join('') +
'}';
const rules = prefixes.map((prefix) => {
return `@${prefix}keyframes ${identifier}${steps}`;
});
return [identifier, rules];
}
/**
* Create CSS keyframes rules and names from a StyleSheet keyframes object.
*/
function processKeyframesValue(keyframesValue) {
if (typeof keyframesValue === 'number') {
throw new Error(`Invalid CSS keyframes type: ${typeof keyframesValue}`);
}
const animationNames = [];
const rules = [];
const value = Array.isArray(keyframesValue)
? keyframesValue
: [keyframesValue];
value.forEach((keyframes) => {
if (typeof keyframes === 'string') {
// Support external animation libraries (identifiers only)
animationNames.push(keyframes);
} else {
// Create rules for each of the keyframes
const [identifier, keyframesRules] = createKeyframes(keyframes);
animationNames.push(identifier);
rules.push(...keyframesRules);
}
});
return [animationNames, rules];
}
@@ -0,0 +1,34 @@
/**
* Copyright (c) Nicolas Gallagher.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import isWebColor from '../../../modules/isWebColor';
import processColor from '../../../exports/processColor';
const normalizeColor = (
color?: number | string,
opacity?: number = 1
): void | string => {
if (color == null) return;
if (typeof color === 'string' && isWebColor(color)) {
return color;
}
const colorInt = processColor(color);
if (colorInt != null) {
const r = (colorInt >> 16) & 255;
const g = (colorInt >> 8) & 255;
const b = colorInt & 255;
const a = ((colorInt >> 24) & 255) / 255;
const alpha = (a * opacity).toFixed(2);
return `rgba(${r},${g},${b},${alpha})`;
}
};
export default normalizeColor;
@@ -0,0 +1,40 @@
/**
* Copyright (c) Nicolas Gallagher.
*
* 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
*/
import unitlessNumbers from './unitlessNumbers';
import normalizeColor from './normalizeColor';
const colorProps = {
backgroundColor: true,
borderColor: true,
borderTopColor: true,
borderRightColor: true,
borderBottomColor: true,
borderLeftColor: true,
color: true,
shadowColor: true,
textDecorationColor: true,
textShadowColor: true
};
export default function normalizeValueWithProperty(
value: any,
property?: ?string
): any {
let returnValue = value;
if (
(property == null || !unitlessNumbers[property]) &&
typeof value === 'number'
) {
returnValue = `${value}px`;
} else if (property != null && colorProps[property]) {
returnValue = normalizeColor(value);
}
return returnValue;
}
@@ -0,0 +1,32 @@
/**
* Copyright (c) Nicolas Gallagher.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import normalizeColor from './normalizeColor';
import normalizeValueWithProperty from './normalizeValueWithProperty';
const defaultOffset = { height: 0, width: 0 };
const resolveShadowValue = (style: Object): void | string => {
const { shadowColor, shadowOffset, shadowOpacity, shadowRadius } = style;
const { height, width } = shadowOffset || defaultOffset;
const offsetX = normalizeValueWithProperty(width);
const offsetY = normalizeValueWithProperty(height);
const blurRadius = normalizeValueWithProperty(shadowRadius || 0);
const color = normalizeColor(shadowColor || 'black', shadowOpacity);
if (
color != null &&
offsetX != null &&
offsetY != null &&
blurRadius != null
) {
return `${offsetX} ${offsetY} ${blurRadius} ${color}`;
}
};
export default resolveShadowValue;
@@ -0,0 +1,76 @@
/**
* Copyright (c) Nicolas Gallagher.
* 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
*/
const unitlessNumbers = {
animationIterationCount: true,
aspectRatio: true,
borderImageOutset: true,
borderImageSlice: true,
borderImageWidth: true,
boxFlex: true,
boxFlexGroup: true,
boxOrdinalGroup: true,
columnCount: true,
flex: true,
flexGrow: true,
flexOrder: true,
flexPositive: true,
flexShrink: true,
flexNegative: true,
fontWeight: true,
gridRow: true,
gridRowEnd: true,
gridRowGap: true,
gridRowStart: true,
gridColumn: true,
gridColumnEnd: true,
gridColumnGap: true,
gridColumnStart: true,
lineClamp: true,
opacity: true,
order: true,
orphans: true,
tabSize: true,
widows: true,
zIndex: true,
zoom: true,
// SVG-related
fillOpacity: true,
floodOpacity: true,
stopOpacity: true,
strokeDasharray: true,
strokeDashoffset: true,
strokeMiterlimit: true,
strokeOpacity: true,
strokeWidth: true,
// transform types
scale: true,
scaleX: true,
scaleY: true,
scaleZ: true,
// RN properties
shadowOpacity: true
};
/**
* Support style names that may come passed in prefixed by adding permutations
* of vendor prefixes.
*/
const prefixes = ['ms', 'Moz', 'O', 'Webkit'];
const prefixKey = (prefix: string, key: string) => {
return prefix + key.charAt(0).toUpperCase() + key.substring(1);
};
Object.keys(unitlessNumbers).forEach((prop) => {
prefixes.forEach((prefix) => {
unitlessNumbers[prefixKey(prefix, prop)] = unitlessNumbers[prop];
});
});
export default unitlessNumbers;
@@ -0,0 +1,40 @@
/**
* Copyright (c) Nicolas Gallagher.
*
* 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
*/
import canUseDOM from '../../../modules/canUseDom';
// $FlowFixMe: HTMLStyleElement is incorrectly typed - https://github.com/facebook/flow/issues/2696
export default function createCSSStyleSheet(
id: string,
rootNode?: Document | ShadowRoot,
textContent?: string
): ?CSSStyleSheet {
if (canUseDOM) {
const root = rootNode != null ? rootNode : document;
let element = root.getElementById(id);
if (element == null) {
element = document.createElement('style');
element.setAttribute('id', id);
if (typeof textContent === 'string') {
element.appendChild(document.createTextNode(textContent));
}
if (root instanceof ShadowRoot) {
root.insertBefore(element, root.firstChild);
} else {
const head = root.head;
if (head) {
head.insertBefore(element, head.firstChild);
}
}
}
// $FlowFixMe: HTMLElement is incorrectly typed
return element.sheet;
} else {
return null;
}
}
@@ -0,0 +1,184 @@
/**
* Copyright (c) Nicolas Gallagher.
*
* 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
*/
type Groups = { [key: number]: { start: ?number, rules: Array<string> } };
type Selectors = { [key: string]: boolean };
export type OrderedCSSStyleSheet = {|
getTextContent: () => string,
insert: (cssText: string, groupValue: number) => void
|};
const slice = Array.prototype.slice;
/**
* Order-based insertion of CSS.
*
* Each rule is associated with a numerically defined group.
* Groups are ordered within the style sheet according to their number, with the
* lowest first.
*
* Groups are implemented using marker rules. The selector of the first rule of
* each group is used only to encode the group number for hydration. An
* alternative implementation could rely on CSSMediaRule, allowing groups to be
* treated as a sub-sheet, but the Edge implementation of CSSMediaRule is
* broken.
* https://developer.mozilla.org/en-US/docs/Web/API/CSSMediaRule
* https://gist.github.com/necolas/aa0c37846ad6bd3b05b727b959e82674
*/
export default function createOrderedCSSStyleSheet(
sheet: ?CSSStyleSheet
): OrderedCSSStyleSheet {
const groups: Groups = {};
const selectors: Selectors = {};
/**
* Hydrate approximate record from any existing rules in the sheet.
*/
if (sheet != null) {
let group;
slice.call(sheet.cssRules).forEach((cssRule, i) => {
const cssText = cssRule.cssText;
// Create record of existing selectors and rules
if (cssText.indexOf('stylesheet-group') > -1) {
group = decodeGroupRule(cssRule);
groups[group] = { start: i, rules: [cssText] };
} else {
const selectorText = getSelectorText(cssText);
if (selectorText != null) {
selectors[selectorText] = true;
groups[group].rules.push(cssText);
}
}
});
}
function sheetInsert(sheet, group, text) {
const orderedGroups = getOrderedGroups(groups);
const groupIndex = orderedGroups.indexOf(group);
const nextGroupIndex = groupIndex + 1;
const nextGroup = orderedGroups[nextGroupIndex];
// Insert rule before the next group, or at the end of the stylesheet
const position =
nextGroup != null && groups[nextGroup].start != null
? groups[nextGroup].start
: sheet.cssRules.length;
const isInserted = insertRuleAt(sheet, text, position);
if (isInserted) {
// Set the starting index of the new group
if (groups[group].start == null) {
groups[group].start = position;
}
// Increment the starting index of all subsequent groups
for (let i = nextGroupIndex; i < orderedGroups.length; i += 1) {
const groupNumber = orderedGroups[i];
const previousStart = groups[groupNumber].start || 0;
groups[groupNumber].start = previousStart + 1;
}
}
return isInserted;
}
const OrderedCSSStyleSheet = {
/**
* The textContent of the style sheet.
*/
getTextContent(): string {
return getOrderedGroups(groups)
.map((group) => {
const rules = groups[group].rules;
// Sorting provides deterministic order of styles in group for
// build-time extraction of the style sheet.
const marker = rules.shift();
rules.sort();
rules.unshift(marker);
return rules.join('\n');
})
.join('\n');
},
/**
* Insert a rule into the style sheet
*/
insert(cssText: string, groupValue: number) {
const group = Number(groupValue);
// Create a new group.
if (groups[group] == null) {
const markerRule = encodeGroupRule(group);
// Create the internal record.
groups[group] = { start: null, rules: [markerRule] };
// Update CSSOM.
if (sheet != null) {
sheetInsert(sheet, group, markerRule);
}
}
// selectorText is more reliable than cssText for insertion checks. The
// browser excludes vendor-prefixed properties and rewrites certain values
// making cssText more likely to be different from what was inserted.
const selectorText = getSelectorText(cssText);
if (selectorText != null && selectors[selectorText] == null) {
// Update the internal records.
selectors[selectorText] = true;
groups[group].rules.push(cssText);
// Update CSSOM.
if (sheet != null) {
const isInserted = sheetInsert(sheet, group, cssText);
if (!isInserted) {
// Revert internal record change if a rule was rejected (e.g.,
// unrecognized pseudo-selector)
groups[group].rules.pop();
}
}
}
}
};
return OrderedCSSStyleSheet;
}
/**
* Helper functions
*/
function encodeGroupRule(group) {
return `[stylesheet-group="${group}"]{}`;
}
const groupPattern = /["']/g;
function decodeGroupRule(cssRule) {
return Number(cssRule.selectorText.split(groupPattern)[1]);
}
function getOrderedGroups(obj: { [key: number]: any }) {
return Object.keys(obj)
.map(Number)
.sort((a, b) => (a > b ? 1 : -1));
}
const selectorPattern = /\s*([,])\s*/g;
function getSelectorText(cssText) {
const selector = cssText.split('{')[0].trim();
return selector !== '' ? selector.replace(selectorPattern, '$1') : null;
}
function insertRuleAt(root, cssText: string, position: number): boolean {
try {
// $FlowFixMe: Flow is missing CSSOM types needed to type 'root'.
root.insertRule(cssText, position);
return true;
} catch (e) {
// JSDOM doesn't support `CSSSMediaRule#insertRule`.
// Also ignore errors that occur from attempting to insert vendor-prefixed selectors.
return false;
}
}
@@ -0,0 +1,90 @@
/**
* Copyright (c) Nicolas Gallagher.
*
* 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
*/
import type { OrderedCSSStyleSheet } from './createOrderedCSSStyleSheet';
import canUseDOM from '../../../modules/canUseDom';
import createCSSStyleSheet from './createCSSStyleSheet';
import createOrderedCSSStyleSheet from './createOrderedCSSStyleSheet';
type Sheet = {
...OrderedCSSStyleSheet,
id: string
};
const defaultId = 'react-native-stylesheet';
const roots = new WeakMap<Node, number>();
const sheets = [];
const initialRules = [
// minimal top-level reset
'html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:rgba(0,0,0,0);}',
'body{margin:0;}',
// minimal form pseudo-element reset
'button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0;}',
'input::-webkit-search-cancel-button,input::-webkit-search-decoration,input::-webkit-search-results-button,input::-webkit-search-results-decoration{display:none;}'
];
export function createSheet(
root?: HTMLElement,
id?: string = defaultId
): Sheet {
let sheet;
if (canUseDOM) {
const rootNode: Node = root != null ? root.getRootNode() : document;
// Create the initial style sheet
if (sheets.length === 0) {
sheet = createOrderedCSSStyleSheet(createCSSStyleSheet(id));
initialRules.forEach((rule) => {
sheet.insert(rule, 0);
});
roots.set(rootNode, sheets.length);
sheets.push(sheet);
} else {
const index = roots.get(rootNode);
if (index == null) {
const initialSheet = sheets[0];
// If we're creating a new sheet, populate it with existing styles
const textContent =
initialSheet != null ? initialSheet.getTextContent() : '';
// Cast rootNode to 'any' because Flow types for getRootNode are wrong
sheet = createOrderedCSSStyleSheet(
createCSSStyleSheet(id, (rootNode: any), textContent)
);
roots.set(rootNode, sheets.length);
sheets.push(sheet);
} else {
sheet = sheets[index];
}
}
} else {
// Create the initial style sheet
if (sheets.length === 0) {
sheet = createOrderedCSSStyleSheet(createCSSStyleSheet(id));
initialRules.forEach((rule) => {
sheet.insert(rule, 0);
});
sheets.push(sheet);
} else {
sheet = sheets[0];
}
}
return {
getTextContent() {
return sheet.getTextContent();
},
id,
insert(cssText: string, groupValue: number) {
sheets.forEach((s) => {
s.insert(cssText, groupValue);
});
}
};
}
@@ -0,0 +1,197 @@
/**
* Copyright (c) Nicolas Gallagher.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import { atomic, classic, inline } from './compiler';
import { createSheet } from './dom';
import { localizeStyle } from 'styleq/transform-localize-style';
import { preprocess } from './preprocess';
import { styleq } from 'styleq';
import { validate } from './validate';
import canUseDOM from '../../modules/canUseDom';
const staticStyleMap: WeakMap<Object, Object> = new WeakMap();
const sheet = createSheet();
const defaultPreprocessOptions = { shadow: true, textShadow: true };
function customStyleq(styles, options: Options = {}) {
const { writingDirection, ...preprocessOptions } = options;
const isRTL = writingDirection === 'rtl';
return styleq.factory({
transform(style) {
const compiledStyle = staticStyleMap.get(style);
if (compiledStyle != null) {
return localizeStyle(compiledStyle, isRTL);
}
return preprocess(style, {
...defaultPreprocessOptions,
...preprocessOptions
});
}
})(styles);
}
function insertRules(compiledOrderedRules) {
compiledOrderedRules.forEach(([rules, order]) => {
if (sheet != null) {
rules.forEach((rule) => {
sheet.insert(rule, order);
});
}
});
}
function compileAndInsertAtomic(style) {
const [compiledStyle, compiledOrderedRules] = atomic(
preprocess(style, defaultPreprocessOptions)
);
insertRules(compiledOrderedRules);
return compiledStyle;
}
function compileAndInsertReset(style, key) {
const [compiledStyle, compiledOrderedRules] = classic(style, key);
insertRules(compiledOrderedRules);
return compiledStyle;
}
/* ----- API ----- */
const absoluteFillObject = {
position: 'absolute',
left: 0,
right: 0,
top: 0,
bottom: 0
};
const absoluteFill = create({ x: { ...absoluteFillObject } }).x;
/**
* create
*/
function create<T: Object>(styles: T): $ReadOnly<T> {
Object.keys(styles).forEach((key) => {
const styleObj = styles[key];
// Only compile at runtime if the style is not already compiled
if (styleObj != null && styleObj.$$css !== true) {
let compiledStyles;
if (key.indexOf('$raw') > -1) {
compiledStyles = compileAndInsertReset(styleObj, key.split('$raw')[0]);
} else {
if (process.env.NODE_ENV !== 'production') {
validate(styleObj);
styles[key] = Object.freeze(styleObj);
}
compiledStyles = compileAndInsertAtomic(styleObj);
}
staticStyleMap.set(styleObj, compiledStyles);
}
});
return styles;
}
/**
* compose
*/
function compose(style1: any, style2: any): any {
if (process.env.NODE_ENV !== 'production') {
/* eslint-disable prefer-rest-params */
const len = arguments.length;
if (len > 2) {
const readableStyles = [...arguments].map((a) => flatten(a));
throw new Error(
`StyleSheet.compose() only accepts 2 arguments, received ${len}: ${JSON.stringify(
readableStyles
)}`
);
}
/* eslint-enable prefer-rest-params */
/*
console.warn(
'StyleSheet.compose(a, b) is deprecated; use array syntax, i.e., [a,b].'
);
*/
}
return [style1, style2];
}
/**
* flatten
*/
function flatten(...styles: any): { [key: string]: any } {
const flatArray = styles.flat(Infinity);
const result = {};
for (let i = 0; i < flatArray.length; i++) {
const style = flatArray[i];
if (style != null && typeof style === 'object') {
// $FlowFixMe
Object.assign(result, style);
}
}
return result;
}
/**
* getSheet
*/
function getSheet(): { id: string, textContent: string } {
return {
id: sheet.id,
textContent: sheet.getTextContent()
};
}
/**
* resolve
*/
type StyleProps = [string, { [key: string]: mixed } | null];
type Options = {
shadow?: boolean,
textShadow?: boolean,
writingDirection: 'ltr' | 'rtl'
};
function StyleSheet(styles: any, options?: Options = {}): StyleProps {
const isRTL = options.writingDirection === 'rtl';
const styleProps: StyleProps = customStyleq(styles, options);
if (Array.isArray(styleProps) && styleProps[1] != null) {
styleProps[1] = inline(styleProps[1], isRTL);
}
return styleProps;
}
StyleSheet.absoluteFill = absoluteFill;
StyleSheet.absoluteFillObject = absoluteFillObject;
StyleSheet.create = create;
StyleSheet.compose = compose;
StyleSheet.flatten = flatten;
StyleSheet.getSheet = getSheet;
// `hairlineWidth` is not implemented using screen density as browsers may
// round sub-pixel values down to `0`, causing the line not to be rendered.
StyleSheet.hairlineWidth = 1;
if (canUseDOM && window.__REACT_DEVTOOLS_GLOBAL_HOOK__) {
window.__REACT_DEVTOOLS_GLOBAL_HOOK__.resolveRNStyle = StyleSheet.flatten;
}
export type IStyleSheet = {
(styles: $ReadOnlyArray<any>, options?: Options): StyleProps,
absoluteFill: Object,
absoluteFillObject: Object,
create: typeof create,
compose: typeof compose,
flatten: typeof flatten,
getSheet: typeof getSheet,
hairlineWidth: number
};
const stylesheet: IStyleSheet = StyleSheet;
export default stylesheet;
@@ -0,0 +1,255 @@
/**
* Copyright (c) Nicolas Gallagher.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import normalizeColor from './compiler/normalizeColor';
import normalizeValueWithProperty from './compiler/normalizeValueWithProperty';
import { warnOnce } from '../../modules/warnOnce';
const emptyObject = {};
/**
* Shadows
*/
const defaultOffset = { height: 0, width: 0 };
export const createBoxShadowValue = (style: Object): void | string => {
const { shadowColor, shadowOffset, shadowOpacity, shadowRadius } = style;
const { height, width } = shadowOffset || defaultOffset;
const offsetX = normalizeValueWithProperty(width);
const offsetY = normalizeValueWithProperty(height);
const blurRadius = normalizeValueWithProperty(shadowRadius || 0);
const color = normalizeColor(shadowColor || 'black', shadowOpacity);
if (
color != null &&
offsetX != null &&
offsetY != null &&
blurRadius != null
) {
return `${offsetX} ${offsetY} ${blurRadius} ${color}`;
}
};
export const createTextShadowValue = (style: Object): void | string => {
const { textShadowColor, textShadowOffset, textShadowRadius } = style;
const { height, width } = textShadowOffset || defaultOffset;
const radius = textShadowRadius || 0;
const offsetX = normalizeValueWithProperty(width);
const offsetY = normalizeValueWithProperty(height);
const blurRadius = normalizeValueWithProperty(radius);
const color = normalizeValueWithProperty(textShadowColor, 'textShadowColor');
if (
color &&
(height !== 0 || width !== 0 || radius !== 0) &&
offsetX != null &&
offsetY != null &&
blurRadius != null
) {
return `${offsetX} ${offsetY} ${blurRadius} ${color}`;
}
};
// { offsetX: 1, offsetY: 2, blurRadius: 3, spreadDistance: 4, color: 'rgba(255, 0, 0)', inset: true }
// => 'rgba(255, 0, 0) 1px 2px 3px 4px inset'
const mapBoxShadow = (boxShadow: Object | string): string => {
if (typeof boxShadow === 'string') {
return boxShadow;
}
const offsetX = normalizeValueWithProperty(boxShadow.offsetX) || 0;
const offsetY = normalizeValueWithProperty(boxShadow.offsetY) || 0;
const blurRadius = normalizeValueWithProperty(boxShadow.blurRadius) || 0;
const spreadDistance =
normalizeValueWithProperty(boxShadow.spreadDistance) || 0;
const color = normalizeColor(boxShadow.color) || 'black';
const position = boxShadow.inset ? 'inset ' : '';
return `${position}${offsetX} ${offsetY} ${blurRadius} ${spreadDistance} ${color}`;
};
export const createBoxShadowArrayValue = (value: Array<Object>): string => {
return value.map(mapBoxShadow).join(', ');
};
// { scale: 2 } => 'scale(2)'
// { translateX: 20 } => 'translateX(20px)'
// { matrix: [1,2,3,4,5,6] } => 'matrix(1,2,3,4,5,6)'
const mapTransform = (transform: Object): string => {
const type = Object.keys(transform)[0];
const value = transform[type];
if (type === 'matrix' || type === 'matrix3d') {
return `${type}(${value.join(',')})`;
} else {
const normalizedValue = normalizeValueWithProperty(value, type);
return `${type}(${normalizedValue})`;
}
};
export const createTransformValue = (value: Array<Object>): string => {
return value.map(mapTransform).join(' ');
};
// [2, '30%', 10] => '2px 30% 10px'
export const createTransformOriginValue = (
value: Array<number | string>
): string => {
return value.map((v) => normalizeValueWithProperty(v)).join(' ');
};
const PROPERTIES_STANDARD: { [key: string]: string } = {
borderBottomEndRadius: 'borderEndEndRadius',
borderBottomStartRadius: 'borderEndStartRadius',
borderTopEndRadius: 'borderStartEndRadius',
borderTopStartRadius: 'borderStartStartRadius',
borderEndColor: 'borderInlineEndColor',
borderEndStyle: 'borderInlineEndStyle',
borderEndWidth: 'borderInlineEndWidth',
borderStartColor: 'borderInlineStartColor',
borderStartStyle: 'borderInlineStartStyle',
borderStartWidth: 'borderInlineStartWidth',
end: 'insetInlineEnd',
marginEnd: 'marginInlineEnd',
marginHorizontal: 'marginInline',
marginStart: 'marginInlineStart',
marginVertical: 'marginBlock',
paddingEnd: 'paddingInlineEnd',
paddingHorizontal: 'paddingInline',
paddingStart: 'paddingInlineStart',
paddingVertical: 'paddingBlock',
start: 'insetInlineStart'
};
const ignoredProps = {
elevation: true,
overlayColor: true,
resizeMode: true,
tintColor: true
};
/**
* Preprocess styles
*/
export const preprocess = <T: {| [key: string]: any |}>(
originalStyle: T,
options?: { shadow?: boolean, textShadow?: boolean } = {}
): T => {
const style = originalStyle || emptyObject;
const nextStyle = {};
// Convert shadow styles
if (
(options.shadow === true,
style.shadowColor != null ||
style.shadowOffset != null ||
style.shadowOpacity != null ||
style.shadowRadius != null)
) {
warnOnce(
'shadowStyles',
`"shadow*" style props are deprecated. Use "boxShadow".`
);
const boxShadowValue = createBoxShadowValue(style);
if (boxShadowValue != null) {
nextStyle.boxShadow = boxShadowValue;
}
}
// Convert text shadow styles
if (
(options.textShadow === true,
style.textShadowColor != null ||
style.textShadowOffset != null ||
style.textShadowRadius != null)
) {
warnOnce(
'textShadowStyles',
`"textShadow*" style props are deprecated. Use "textShadow".`
);
const textShadowValue = createTextShadowValue(style);
if (textShadowValue != null && nextStyle.textShadow == null) {
const { textShadow } = style;
const value = textShadow
? `${textShadow}, ${textShadowValue}`
: textShadowValue;
nextStyle.textShadow = value;
}
}
for (const originalProp in style) {
if (
// Ignore some React Native styles
ignoredProps[originalProp] != null ||
originalProp === 'shadowColor' ||
originalProp === 'shadowOffset' ||
originalProp === 'shadowOpacity' ||
originalProp === 'shadowRadius' ||
originalProp === 'textShadowColor' ||
originalProp === 'textShadowOffset' ||
originalProp === 'textShadowRadius'
) {
continue;
}
const originalValue = style[originalProp];
const prop = PROPERTIES_STANDARD[originalProp] || originalProp;
let value = originalValue;
if (
!Object.prototype.hasOwnProperty.call(style, originalProp) ||
(prop !== originalProp && style[prop] != null)
) {
continue;
}
if (prop === 'aspectRatio' && typeof value === 'number') {
nextStyle[prop] = value.toString();
} else if (prop === 'boxShadow') {
if (Array.isArray(value)) {
value = createBoxShadowArrayValue(value);
}
const { boxShadow } = nextStyle;
nextStyle.boxShadow = boxShadow ? `${value}, ${boxShadow}` : value;
} else if (prop === 'fontVariant') {
if (Array.isArray(value) && value.length > 0) {
/*
warnOnce(
'fontVariant',
'"fontVariant" style array value is deprecated. Use space-separated values.'
);
*/
value = value.join(' ');
}
nextStyle[prop] = value;
} else if (prop === 'textAlignVertical') {
/*
warnOnce(
'textAlignVertical',
'"textAlignVertical" style is deprecated. Use "verticalAlign".'
);
*/
if (style.verticalAlign == null) {
nextStyle.verticalAlign = value === 'center' ? 'middle' : value;
}
} else if (prop === 'transform') {
if (Array.isArray(value)) {
value = createTransformValue(value);
}
nextStyle.transform = value;
} else if (prop === 'transformOrigin') {
if (Array.isArray(value)) {
value = createTransformOriginValue(value);
}
nextStyle.transformOrigin = value;
} else {
nextStyle[prop] = value;
}
}
// $FlowIgnore
return nextStyle;
};
export default preprocess;
@@ -0,0 +1,93 @@
/**
* Copyright (c) Nicolas Gallagher.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @noflow
*/
import valueParser from 'postcss-value-parser';
const invalidShortforms = {
background: true,
borderBottom: true,
borderLeft: true,
borderRight: true,
borderTop: true,
font: true,
grid: true,
outline: true,
textDecoration: true
};
const invalidMultiValueShortforms = {
flex: true,
margin: true,
padding: true,
borderColor: true,
borderRadius: true,
borderStyle: true,
borderWidth: true,
inset: true,
insetBlock: true,
insetInline: true,
marginBlock: true,
marginInline: true,
marginHorizontal: true,
marginVertical: true,
paddingBlock: true,
paddingInline: true,
paddingHorizontal: true,
paddingVertical: true,
overflow: true,
overscrollBehavior: true,
backgroundPosition: true
};
function error(message) {
console.error(message);
}
export function validate(obj: Object) {
for (const k in obj) {
const prop = k.trim();
const value = obj[prop];
let isInvalid = false;
if (value === null) {
continue;
}
if (typeof value === 'string' && value.indexOf('!important') > -1) {
error(
`Invalid style declaration "${prop}:${value}". Values cannot include "!important"`
);
isInvalid = true;
} else {
let suggestion = '';
if (prop === 'animation' || prop === 'animationName') {
suggestion = 'Did you mean "animationKeyframes"?';
isInvalid = true;
} else if (prop === 'direction') {
suggestion = 'Did you mean "writingDirection"?';
isInvalid = true;
} else if (invalidShortforms[prop]) {
suggestion = 'Please use long-form properties.';
isInvalid = true;
} else if (invalidMultiValueShortforms[prop]) {
if (typeof value === 'string' && valueParser(value).nodes.length > 1) {
suggestion = `Value is "${value}" but only single values are supported.`;
isInvalid = true;
}
}
if (suggestion !== '') {
error(`Invalid style property of "${prop}". ${suggestion}`);
}
}
if (isInvalid) {
delete obj[k];
}
}
}
@@ -0,0 +1,236 @@
/**
* Copyright (c) Nicolas Gallagher.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
'use client';
import type { ColorValue } from '../../types';
import type { ViewProps } from '../View';
import * as React from 'react';
import createElement from '../createElement';
import multiplyStyleLengthValue from '../../modules/multiplyStyleLengthValue';
import StyleSheet from '../StyleSheet';
import View from '../View';
type SwitchProps = {
...ViewProps,
activeThumbColor?: ColorValue,
activeTrackColor?: ColorValue,
disabled?: boolean,
onValueChange?: (e: any) => void,
thumbColor?: ColorValue,
trackColor?: ColorValue | {| false: ColorValue, true: ColorValue |},
value?: boolean
};
const emptyObject = {};
const thumbDefaultBoxShadow = '0px 1px 3px rgba(0,0,0,0.5)';
const thumbFocusedBoxShadow = `${thumbDefaultBoxShadow}, 0 0 0 10px rgba(0,0,0,0.1)`;
const defaultActiveTrackColor = '#A3D3CF';
const defaultTrackColor = '#939393';
const defaultDisabledTrackColor = '#D5D5D5';
const defaultActiveThumbColor = '#009688';
const defaultThumbColor = '#FAFAFA';
const defaultDisabledThumbColor = '#BDBDBD';
const Switch: React.AbstractComponent<
SwitchProps,
React.ElementRef<typeof View>
> = React.forwardRef((props, forwardedRef) => {
const {
'aria-label': ariaLabel,
accessibilityLabel,
activeThumbColor,
activeTrackColor,
disabled = false,
onValueChange,
style = emptyObject,
thumbColor,
trackColor,
value = false,
...other
} = props;
const thumbRef = React.useRef(null);
function handleChange(event: Object) {
if (onValueChange != null) {
onValueChange(event.nativeEvent.target.checked);
}
}
function handleFocusState(event: Object) {
const isFocused = event.nativeEvent.type === 'focus';
const boxShadow = isFocused ? thumbFocusedBoxShadow : thumbDefaultBoxShadow;
if (thumbRef.current != null) {
thumbRef.current.style.boxShadow = boxShadow;
}
}
const { height: styleHeight, width: styleWidth } = StyleSheet.flatten(style);
const height = styleHeight || '20px';
const minWidth = multiplyStyleLengthValue(height, 2);
const width = styleWidth > minWidth ? styleWidth : minWidth;
const trackBorderRadius = multiplyStyleLengthValue(height, 0.5);
const trackCurrentColor = (function () {
if (value === true) {
if (trackColor != null && typeof trackColor === 'object') {
return trackColor.true;
} else {
return activeTrackColor ?? defaultActiveTrackColor;
}
} else {
if (trackColor != null && typeof trackColor === 'object') {
return trackColor.false;
} else {
return trackColor ?? defaultTrackColor;
}
}
})();
const thumbCurrentColor = value
? activeThumbColor ?? defaultActiveThumbColor
: thumbColor ?? defaultThumbColor;
const thumbHeight = height;
const thumbWidth = thumbHeight;
const rootStyle = [
styles.root,
style,
disabled && styles.cursorDefault,
{ height, width }
];
const disabledTrackColor = (function () {
if (value === true) {
if (
(typeof activeTrackColor === 'string' && activeTrackColor != null) ||
(typeof trackColor === 'object' && trackColor?.true)
) {
return trackCurrentColor;
} else {
return defaultDisabledTrackColor;
}
} else {
if (
(typeof trackColor === 'string' && trackColor != null) ||
(typeof trackColor === 'object' && trackColor?.false)
) {
return trackCurrentColor;
} else {
return defaultDisabledTrackColor;
}
}
})();
const disabledThumbColor = (function () {
if (value === true) {
if (activeThumbColor == null) {
return defaultDisabledThumbColor;
} else {
return thumbCurrentColor;
}
} else {
if (thumbColor == null) {
return defaultDisabledThumbColor;
} else {
return thumbCurrentColor;
}
}
})();
const trackStyle = [
styles.track,
{
backgroundColor: disabled ? disabledTrackColor : trackCurrentColor,
borderRadius: trackBorderRadius
}
];
const thumbStyle = [
styles.thumb,
value && styles.thumbActive,
{
backgroundColor: disabled ? disabledThumbColor : thumbCurrentColor,
height: thumbHeight,
marginStart: value ? multiplyStyleLengthValue(thumbWidth, -1) : 0,
width: thumbWidth
}
];
const nativeControl = createElement('input', {
'aria-label': ariaLabel || accessibilityLabel,
checked: value,
disabled: disabled,
onBlur: handleFocusState,
onChange: handleChange,
onFocus: handleFocusState,
ref: forwardedRef,
style: [styles.nativeControl, styles.cursorInherit],
type: 'checkbox',
role: 'switch'
});
return (
<View {...other} style={rootStyle}>
<View style={trackStyle} />
<View ref={thumbRef} style={thumbStyle} />
{nativeControl}
</View>
);
});
Switch.displayName = 'Switch';
const styles = StyleSheet.create({
root: {
cursor: 'pointer',
userSelect: 'none'
},
cursorDefault: {
cursor: 'default'
},
cursorInherit: {
cursor: 'inherit'
},
track: {
forcedColorAdjust: 'none',
...StyleSheet.absoluteFillObject,
height: '70%',
margin: 'auto',
transitionDuration: '0.1s',
width: '100%'
},
thumb: {
forcedColorAdjust: 'none',
alignSelf: 'flex-start',
borderRadius: '100%',
boxShadow: thumbDefaultBoxShadow,
start: '0%',
transform: 'translateZ(0)',
transitionDuration: '0.1s'
},
thumbActive: {
insetInlineStart: '100%'
},
nativeControl: {
...StyleSheet.absoluteFillObject,
height: '100%',
margin: 0,
appearance: 'none',
padding: 0,
width: '100%'
}
});
export default Switch;
@@ -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
*/
'use client';
import type { Context } from 'react';
import { createContext } from 'react';
const TextAncestorContext = createContext(false);
export default (TextAncestorContext: Context<boolean>);
+241
View File
@@ -0,0 +1,241 @@
/**
* Copyright (c) Nicolas Gallagher.
* 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
*/
'use client';
import type { PlatformMethods } from '../../types';
import type { TextProps } from './types';
import * as React from 'react';
import createElement from '../createElement';
import * as forwardedProps from '../../modules/forwardedProps';
import pick from '../../modules/pick';
import useElementLayout from '../../modules/useElementLayout';
import useMergeRefs from '../../modules/useMergeRefs';
import usePlatformMethods from '../../modules/usePlatformMethods';
import useResponderEvents from '../../modules/useResponderEvents';
import StyleSheet from '../StyleSheet';
import TextAncestorContext from './TextAncestorContext';
import { useLocaleContext, getLocaleDirection } from '../../modules/useLocale';
//import { warnOnce } from '../../modules/warnOnce';
const forwardPropsList = Object.assign(
{},
forwardedProps.defaultProps,
forwardedProps.accessibilityProps,
forwardedProps.clickProps,
forwardedProps.focusProps,
forwardedProps.keyboardProps,
forwardedProps.mouseProps,
forwardedProps.touchProps,
forwardedProps.styleProps,
{
href: true,
lang: true,
pointerEvents: true
}
);
const pickProps = (props) => pick(props, forwardPropsList);
const Text: React.AbstractComponent<TextProps, HTMLElement & PlatformMethods> =
React.forwardRef((props, forwardedRef) => {
const {
hrefAttrs,
numberOfLines,
onClick,
onLayout,
onPress,
onMoveShouldSetResponder,
onMoveShouldSetResponderCapture,
onResponderEnd,
onResponderGrant,
onResponderMove,
onResponderReject,
onResponderRelease,
onResponderStart,
onResponderTerminate,
onResponderTerminationRequest,
onScrollShouldSetResponder,
onScrollShouldSetResponderCapture,
onSelectionChangeShouldSetResponder,
onSelectionChangeShouldSetResponderCapture,
onStartShouldSetResponder,
onStartShouldSetResponderCapture,
selectable,
...rest
} = props;
/*
if (selectable != null) {
warnOnce(
'selectable',
'selectable prop is deprecated. Use styles.userSelect.'
);
}
*/
const hasTextAncestor = React.useContext(TextAncestorContext);
const hostRef = React.useRef(null);
const { direction: contextDirection } = useLocaleContext();
useElementLayout(hostRef, onLayout);
useResponderEvents(hostRef, {
onMoveShouldSetResponder,
onMoveShouldSetResponderCapture,
onResponderEnd,
onResponderGrant,
onResponderMove,
onResponderReject,
onResponderRelease,
onResponderStart,
onResponderTerminate,
onResponderTerminationRequest,
onScrollShouldSetResponder,
onScrollShouldSetResponderCapture,
onSelectionChangeShouldSetResponder,
onSelectionChangeShouldSetResponderCapture,
onStartShouldSetResponder,
onStartShouldSetResponderCapture
});
const handleClick = React.useCallback(
(e) => {
if (onClick != null) {
onClick(e);
} else if (onPress != null) {
e.stopPropagation();
onPress(e);
}
},
[onClick, onPress]
);
let component = hasTextAncestor ? 'span' : 'div';
const langDirection =
props.lang != null ? getLocaleDirection(props.lang) : null;
const componentDirection = props.dir || langDirection;
const writingDirection = componentDirection || contextDirection;
const supportedProps = pickProps(rest);
supportedProps.dir = componentDirection;
// 'auto' by default allows browsers to infer writing direction (root elements only)
if (!hasTextAncestor) {
supportedProps.dir =
componentDirection != null ? componentDirection : 'auto';
}
if (onClick || onPress) {
supportedProps.onClick = handleClick;
}
supportedProps.style = [
numberOfLines != null &&
numberOfLines > 1 && { WebkitLineClamp: numberOfLines },
hasTextAncestor === true ? styles.textHasAncestor$raw : styles.text$raw,
numberOfLines === 1 && styles.textOneLine,
numberOfLines != null && numberOfLines > 1 && styles.textMultiLine,
props.style,
selectable === true && styles.selectable,
selectable === false && styles.notSelectable,
onPress && styles.pressable
];
if (props.href != null) {
component = 'a';
if (hrefAttrs != null) {
const { download, rel, target } = hrefAttrs;
if (download != null) {
supportedProps.download = download;
}
if (rel != null) {
supportedProps.rel = rel;
}
if (typeof target === 'string') {
supportedProps.target =
target.charAt(0) !== '_' ? '_' + target : target;
}
}
}
const platformMethodsRef = usePlatformMethods(supportedProps);
const setRef = useMergeRefs(hostRef, platformMethodsRef, forwardedRef);
supportedProps.ref = setRef;
const element = createElement(component, supportedProps, {
writingDirection
});
return hasTextAncestor ? (
element
) : (
<TextAncestorContext.Provider value={true}>
{element}
</TextAncestorContext.Provider>
);
});
Text.displayName = 'Text';
const textStyle = {
backgroundColor: 'transparent',
border: '0 solid black',
boxSizing: 'border-box',
color: 'black',
display: 'inline',
font: '14px System',
listStyle: 'none',
margin: 0,
padding: 0,
position: 'relative',
textAlign: 'start',
textDecoration: 'none',
whiteSpace: 'pre-wrap',
wordWrap: 'break-word'
};
const styles = StyleSheet.create({
text$raw: textStyle,
textHasAncestor$raw: {
...textStyle,
color: 'inherit',
font: 'inherit',
textAlign: 'inherit',
whiteSpace: 'inherit'
},
textOneLine: {
maxWidth: '100%',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
wordWrap: 'normal'
},
// See #13
textMultiLine: {
display: '-webkit-box',
maxWidth: '100%',
overflow: 'clip',
textOverflow: 'ellipsis',
WebkitBoxOrient: 'vertical'
},
notSelectable: {
userSelect: 'none'
},
selectable: {
userSelect: 'text'
},
pressable: {
cursor: 'pointer'
}
});
export default Text;
+121
View File
@@ -0,0 +1,121 @@
/**
* Copyright (c) Nicolas Gallagher.
* 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
*/
import type { ColorValue, GenericStyleProp } from '../../types';
import type { ViewProps, ViewStyle } from '../View/types';
type FontWeightValue =
| 'normal'
| 'bold'
| '100'
| '200'
| '300'
| '400'
| '500'
| '600'
| '700'
| '800'
| '900';
type NumberOrString = number | string;
export type TextStyle = {
...ViewStyle,
color?: ?ColorValue,
fontFamily?: ?string,
fontFeatureSettings?: ?string,
fontSize?: ?NumberOrString,
fontStyle?: 'italic' | 'normal',
fontWeight?: ?FontWeightValue,
fontVariant?: $ReadOnlyArray<
| 'small-caps'
| 'oldstyle-nums'
| 'lining-nums'
| 'tabular-nums'
| 'proportional-nums'
>,
letterSpacing?: ?NumberOrString,
lineHeight?: ?NumberOrString,
textAlign?:
| 'center'
| 'end'
| 'inherit'
| 'justify'
| 'justify-all'
| 'left'
| 'right'
| 'start',
textDecorationColor?: ?ColorValue,
textDecorationLine?:
| 'none'
| 'underline'
| 'line-through'
| 'underline line-through',
textDecorationStyle?: 'solid' | 'double' | 'dotted' | 'dashed',
textIndent?: ?NumberOrString,
textOverflow?: ?string,
textRendering?:
| 'auto'
| 'geometricPrecision'
| 'optimizeLegibility'
| 'optimizeSpeed',
textShadow?: ?string,
textShadowColor?: ?ColorValue,
textShadowOffset?: {| width?: number, height?: number |},
textShadowRadius?: ?number,
textTransform?: 'capitalize' | 'lowercase' | 'none' | 'uppercase',
unicodeBidi?:
| 'normal'
| 'bidi-override'
| 'embed'
| 'isolate'
| 'isolate-override'
| 'plaintext',
userSelect?: 'none' | 'text',
verticalAlign?: ?string,
whiteSpace?: ?string,
wordBreak?: 'normal' | 'break-all' | 'break-word' | 'keep-all',
wordWrap?: ?string,
writingDirection?: 'auto' | 'ltr' | 'rtl',
/* @platform web */
MozOsxFontSmoothing?: ?string,
WebkitFontSmoothing?: ?string,
// deprecated
textAlignVertical?: ?string
};
export type TextProps = {
...ViewProps,
dir?: 'auto' | 'ltr' | 'rtl',
numberOfLines?: ?number,
role?:
| 'button'
| 'header'
| 'heading'
| 'label'
| 'link'
| 'listitem'
| 'none'
| 'text',
style?: GenericStyleProp<TextStyle>,
testID?: ?string,
// @deprecated
accessibilityRole?:
| 'button'
| 'header'
| 'heading'
| 'label'
| 'link'
| 'listitem'
| 'none'
| 'text',
onPress?: (e: any) => void,
selectable?: boolean
};
@@ -0,0 +1,484 @@
/**
* Copyright (c) Nicolas Gallagher.
* 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
*/
'use client';
import type { PlatformMethods } from '../../types';
import type { TextInputProps } from './types';
import * as React from 'react';
import createElement from '../createElement';
import * as forwardedProps from '../../modules/forwardedProps';
import pick from '../../modules/pick';
import useElementLayout from '../../modules/useElementLayout';
import useLayoutEffect from '../../modules/useLayoutEffect';
import useMergeRefs from '../../modules/useMergeRefs';
import usePlatformMethods from '../../modules/usePlatformMethods';
import useResponderEvents from '../../modules/useResponderEvents';
import { getLocaleDirection, useLocaleContext } from '../../modules/useLocale';
import StyleSheet from '../StyleSheet';
import TextInputState from '../../modules/TextInputState';
//import { warnOnce } from '../../modules/warnOnce';
/**
* Determines whether a 'selection' prop differs from a node's existing
* selection state.
*/
const isSelectionStale = (node, selection) => {
const { selectionEnd, selectionStart } = node;
const { start, end } = selection;
return start !== selectionStart || end !== selectionEnd;
};
/**
* Certain input types do no support 'selectSelectionRange' and will throw an
* error.
*/
const setSelection = (node, selection) => {
if (isSelectionStale(node, selection)) {
const { start, end } = selection;
try {
node.setSelectionRange(start, end || start);
} catch (e) {}
}
};
const forwardPropsList = Object.assign(
{},
forwardedProps.defaultProps,
forwardedProps.accessibilityProps,
forwardedProps.clickProps,
forwardedProps.focusProps,
forwardedProps.keyboardProps,
forwardedProps.mouseProps,
forwardedProps.touchProps,
forwardedProps.styleProps,
{
autoCapitalize: true,
autoComplete: true,
autoCorrect: true,
autoFocus: true,
defaultValue: true,
disabled: true,
lang: true,
maxLength: true,
onChange: true,
onScroll: true,
placeholder: true,
pointerEvents: true,
readOnly: true,
rows: true,
spellCheck: true,
value: true,
type: true
}
);
const pickProps = (props) => pick(props, forwardPropsList);
// If an Input Method Editor is processing key input, the 'keyCode' is 229.
// https://www.w3.org/TR/uievents/#determine-keydown-keyup-keyCode
function isEventComposing(nativeEvent) {
return nativeEvent.isComposing || nativeEvent.keyCode === 229;
}
let focusTimeout: ?TimeoutID = null;
const TextInput: React.AbstractComponent<
TextInputProps,
HTMLElement & PlatformMethods
> = React.forwardRef((props, forwardedRef) => {
const {
autoCapitalize = 'sentences',
autoComplete,
autoCompleteType,
autoCorrect = true,
blurOnSubmit,
caretHidden,
clearTextOnFocus,
dir,
editable,
enterKeyHint,
inputMode,
keyboardType,
multiline = false,
numberOfLines,
onBlur,
onChange,
onChangeText,
onContentSizeChange,
onFocus,
onKeyPress,
onLayout,
onMoveShouldSetResponder,
onMoveShouldSetResponderCapture,
onResponderEnd,
onResponderGrant,
onResponderMove,
onResponderReject,
onResponderRelease,
onResponderStart,
onResponderTerminate,
onResponderTerminationRequest,
onScrollShouldSetResponder,
onScrollShouldSetResponderCapture,
onSelectionChange,
onSelectionChangeShouldSetResponder,
onSelectionChangeShouldSetResponderCapture,
onStartShouldSetResponder,
onStartShouldSetResponderCapture,
onSubmitEditing,
placeholderTextColor,
readOnly = false,
returnKeyType,
rows,
secureTextEntry = false,
selection,
selectTextOnFocus,
showSoftInputOnFocus,
spellCheck
} = props;
let type;
let _inputMode;
if (inputMode != null) {
_inputMode = inputMode;
if (inputMode === 'email') {
type = 'email';
} else if (inputMode === 'tel') {
type = 'tel';
} else if (inputMode === 'search') {
type = 'search';
} else if (inputMode === 'url') {
type = 'url';
} else {
type = 'text';
}
} else if (keyboardType != null) {
// warnOnce('keyboardType', 'keyboardType is deprecated. Use inputMode.');
switch (keyboardType) {
case 'email-address':
type = 'email';
break;
case 'number-pad':
case 'numeric':
_inputMode = 'numeric';
break;
case 'decimal-pad':
_inputMode = 'decimal';
break;
case 'phone-pad':
type = 'tel';
break;
case 'search':
case 'web-search':
type = 'search';
break;
case 'url':
type = 'url';
break;
default:
type = 'text';
}
}
if (secureTextEntry) {
type = 'password';
}
const dimensions = React.useRef({ height: null, width: null });
const hostRef = React.useRef(null);
const prevSelection = React.useRef(null);
const prevSecureTextEntry = React.useRef(false);
React.useEffect(() => {
if (hostRef.current && prevSelection.current) {
setSelection(hostRef.current, prevSelection.current);
}
prevSecureTextEntry.current = secureTextEntry;
}, [secureTextEntry]);
const handleContentSizeChange = React.useCallback(
(hostNode) => {
if (multiline && onContentSizeChange && hostNode != null) {
const newHeight = hostNode.scrollHeight;
const newWidth = hostNode.scrollWidth;
if (
newHeight !== dimensions.current.height ||
newWidth !== dimensions.current.width
) {
dimensions.current.height = newHeight;
dimensions.current.width = newWidth;
onContentSizeChange({
nativeEvent: {
contentSize: {
height: dimensions.current.height,
width: dimensions.current.width
}
}
});
}
}
},
[multiline, onContentSizeChange]
);
const imperativeRef = React.useMemo(
() => (hostNode) => {
// TextInput needs to add more methods to the hostNode in addition to those
// added by `usePlatformMethods`. This is temporarily until an API like
// `TextInput.clear(hostRef)` is added to React Native.
if (hostNode != null) {
hostNode.clear = function () {
if (hostNode != null) {
hostNode.value = '';
}
};
hostNode.isFocused = function () {
return (
hostNode != null &&
TextInputState.currentlyFocusedField() === hostNode
);
};
handleContentSizeChange(hostNode);
}
},
[handleContentSizeChange]
);
function handleBlur(e) {
TextInputState._currentlyFocusedNode = null;
if (onBlur) {
e.nativeEvent.text = e.target.value;
onBlur(e);
}
}
function handleChange(e) {
const hostNode = e.target;
const text = hostNode.value;
e.nativeEvent.text = text;
handleContentSizeChange(hostNode);
if (onChange) {
onChange(e);
}
if (onChangeText) {
onChangeText(text);
}
}
function handleFocus(e) {
const hostNode = e.target;
if (onFocus) {
e.nativeEvent.text = hostNode.value;
onFocus(e);
}
if (hostNode != null) {
TextInputState._currentlyFocusedNode = hostNode;
if (clearTextOnFocus) {
hostNode.value = '';
}
if (selectTextOnFocus) {
// Safari requires selection to occur in a setTimeout
if (focusTimeout != null) {
clearTimeout(focusTimeout);
}
focusTimeout = setTimeout(() => {
// Check if the input is still focused after the timeout
// (see #2704)
if (hostNode != null && document.activeElement === hostNode) {
hostNode.select();
}
}, 0);
}
}
}
function handleKeyDown(e) {
const hostNode = e.target;
// Prevent key events bubbling (see #612)
e.stopPropagation();
const blurOnSubmitDefault = !multiline;
const shouldBlurOnSubmit =
blurOnSubmit == null ? blurOnSubmitDefault : blurOnSubmit;
const nativeEvent = e.nativeEvent;
const isComposing = isEventComposing(nativeEvent);
if (onKeyPress) {
onKeyPress(e);
}
if (
e.key === 'Enter' &&
!e.shiftKey &&
// Do not call submit if composition is occuring.
!isComposing &&
!e.isDefaultPrevented()
) {
if ((blurOnSubmit || !multiline) && onSubmitEditing) {
// prevent "Enter" from inserting a newline or submitting a form
e.preventDefault();
nativeEvent.text = e.target.value;
onSubmitEditing(e);
}
if (shouldBlurOnSubmit && hostNode != null) {
setTimeout(() => hostNode.blur(), 0);
}
}
}
function handleSelectionChange(e) {
try {
const { selectionStart, selectionEnd } = e.target;
const selection = {
start: selectionStart,
end: selectionEnd
};
if (onSelectionChange) {
e.nativeEvent.selection = selection;
e.nativeEvent.text = e.target.value;
onSelectionChange(e);
}
if (prevSecureTextEntry.current === secureTextEntry) {
prevSelection.current = selection;
}
} catch (e) {}
}
useLayoutEffect(() => {
const node = hostRef.current;
if (node != null && selection != null) {
setSelection(node, selection);
}
if (document.activeElement === node) {
TextInputState._currentlyFocusedNode = node;
}
}, [hostRef, selection]);
const component = multiline ? 'textarea' : 'input';
useElementLayout(hostRef, onLayout);
useResponderEvents(hostRef, {
onMoveShouldSetResponder,
onMoveShouldSetResponderCapture,
onResponderEnd,
onResponderGrant,
onResponderMove,
onResponderReject,
onResponderRelease,
onResponderStart,
onResponderTerminate,
onResponderTerminationRequest,
onScrollShouldSetResponder,
onScrollShouldSetResponderCapture,
onSelectionChangeShouldSetResponder,
onSelectionChangeShouldSetResponderCapture,
onStartShouldSetResponder,
onStartShouldSetResponderCapture
});
const { direction: contextDirection } = useLocaleContext();
const supportedProps = pickProps(props);
supportedProps.autoCapitalize = autoCapitalize;
supportedProps.autoComplete = autoComplete || autoCompleteType || 'on';
supportedProps.autoCorrect = autoCorrect ? 'on' : 'off';
// 'auto' by default allows browsers to infer writing direction
supportedProps.dir = dir !== undefined ? dir : 'auto';
/*
if (returnKeyType != null) {
warnOnce('returnKeyType', 'returnKeyType is deprecated. Use enterKeyHint.');
}
*/
supportedProps.enterKeyHint = enterKeyHint || returnKeyType;
supportedProps.inputMode = _inputMode;
supportedProps.onBlur = handleBlur;
supportedProps.onChange = handleChange;
supportedProps.onFocus = handleFocus;
supportedProps.onKeyDown = handleKeyDown;
supportedProps.onSelect = handleSelectionChange;
/*
if (editable != null) {
warnOnce('editable', 'editable is deprecated. Use readOnly.');
}
*/
supportedProps.readOnly = readOnly === true || editable === false;
/*
if (numberOfLines != null) {
warnOnce(
'numberOfLines',
'TextInput numberOfLines is deprecated. Use rows.'
);
}
*/
supportedProps.rows = multiline ? (rows != null ? rows : numberOfLines) : 1;
supportedProps.spellCheck = spellCheck != null ? spellCheck : autoCorrect;
supportedProps.style = [
{ '--placeholderTextColor': placeholderTextColor },
styles.textinput$raw,
styles.placeholder,
props.style,
caretHidden && styles.caretHidden
];
supportedProps.type = multiline ? undefined : type;
supportedProps.virtualkeyboardpolicy =
showSoftInputOnFocus === false ? 'manual' : 'auto';
const platformMethodsRef = usePlatformMethods(supportedProps);
const setRef = useMergeRefs(
hostRef,
platformMethodsRef,
imperativeRef,
forwardedRef
);
supportedProps.ref = setRef;
const langDirection =
props.lang != null ? getLocaleDirection(props.lang) : null;
const componentDirection = props.dir || langDirection;
const writingDirection = componentDirection || contextDirection;
const element = createElement(component, supportedProps, {
writingDirection
});
return element;
});
TextInput.displayName = 'TextInput';
// $FlowFixMe
TextInput.State = TextInputState;
const styles = StyleSheet.create({
textinput$raw: {
MozAppearance: 'textfield',
WebkitAppearance: 'none',
backgroundColor: 'transparent',
border: '0 solid black',
borderRadius: 0,
boxSizing: 'border-box',
font: '14px System',
margin: 0,
padding: 0,
resize: 'none'
},
placeholder: {
placeholderTextColor: 'var(--placeholderTextColor)'
},
caretHidden: {
caretColor: 'transparent'
}
});
export default TextInput;
@@ -0,0 +1,97 @@
/**
* Copyright (c) Nicolas Gallagher.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import type { ColorValue, GenericStyleProp } from '../../types';
import type { TextStyle } from '../Text/types';
import type { ViewProps } from '../View/types';
export type TextInputStyle = {
...TextStyle,
caretColor?: ColorValue,
resize?: 'none' | 'vertical' | 'horizontal' | 'both'
};
export type TextInputProps = {
...ViewProps,
autoCapitalize?: 'characters' | 'none' | 'sentences' | 'words',
autoComplete?: ?string,
autoCompleteType?: ?string, // Compat with React Native (Bug react-native#26003)
autoCorrect?: ?boolean,
autoFocus?: ?boolean,
blurOnSubmit?: ?boolean,
caretHidden?: ?boolean,
clearTextOnFocus?: ?boolean,
defaultValue?: ?string,
dir?: ?('auto' | 'ltr' | 'rtl'),
disabled?: ?boolean,
enterKeyHint?:
| 'enter'
| 'done'
| 'go'
| 'next'
| 'previous'
| 'search'
| 'send',
inputAccessoryViewID?: ?string,
inputMode?:
| 'decimal'
| 'email'
| 'none'
| 'numeric'
| 'search'
| 'tel'
| 'text'
| 'url',
maxLength?: ?number,
multiline?: ?boolean,
onChange?: (e: any) => void,
onChangeText?: (e: string) => void,
onContentSizeChange?: (e: any) => void,
onEndEditing?: (e: any) => void,
onKeyPress?: (e: any) => void,
onSelectionChange?: (e: any) => void,
onScroll?: (e: any) => void,
onSubmitEditing?: (e: any) => void,
placeholder?: ?string,
placeholderTextColor?: ?ColorValue,
readOnly?: ?boolean,
rows?: ?number,
secureTextEntry?: ?boolean,
selectTextOnFocus?: ?boolean,
selection?: {|
start: number,
end?: number
|},
selectionColor?: ?ColorValue,
showSoftInputOnFocus?: ?boolean,
spellCheck?: ?boolean,
style?: ?GenericStyleProp<TextInputStyle>,
value?: ?string,
// deprecated
editable?: ?boolean,
keyboardType?:
| 'default'
| 'email-address'
| 'number-pad'
| 'numbers-and-punctuation'
| 'numeric'
| 'phone-pad'
| 'search'
| 'url'
| 'web-search',
numberOfLines?: ?number,
returnKeyType?:
| 'enter'
| 'done'
| 'go'
| 'next'
| 'previous'
| 'search'
| 'send'
};
@@ -0,0 +1,37 @@
/**
* Copyright (c) Nicolas Gallagher.
* 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
*/
import PooledClass from '../../vendor/react-native/PooledClass';
const twoArgumentPooler = PooledClass.twoArgumentPooler;
/**
* PooledClass representing the bounding rectangle of a region.
*/
function BoundingDimensions(width: number, height: number) {
this.width = width;
this.height = height;
}
BoundingDimensions.prototype.destructor = function () {
this.width = null;
this.height = null;
};
BoundingDimensions.getPooledFromElement = function (element: HTMLElement): any {
return BoundingDimensions.getPooled(
element.offsetWidth,
element.offsetHeight
);
};
PooledClass.addPoolingTo(BoundingDimensions, twoArgumentPooler);
export default BoundingDimensions;
@@ -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.
*
* @noflow
*/
import PooledClass from '../../vendor/react-native/PooledClass';
const twoArgumentPooler = PooledClass.twoArgumentPooler;
function Position(left, top) {
this.left = left;
this.top = top;
}
Position.prototype.destructor = function () {
this.left = null;
this.top = null;
};
PooledClass.addPoolingTo(Position, twoArgumentPooler);
export default Position;
@@ -0,0 +1,23 @@
/**
* 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
*/
import invariant from 'fbjs/lib/invariant';
const ensurePositiveDelayProps = (props: any) => {
invariant(
!(
props.delayPressIn < 0 ||
props.delayPressOut < 0 ||
props.delayLongPress < 0
),
'Touchable components cannot have negative delay properties'
);
};
export default ensurePositiveDelayProps;
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,214 @@
/**
* 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 client';
import type { ColorValue } from '../../types';
import type { Props as TouchableWithoutFeedbackProps } from '../TouchableWithoutFeedback';
import type { ViewProps } from '../View';
import * as React from 'react';
import { useCallback, useMemo, useState, useRef } from 'react';
import useMergeRefs from '../../modules/useMergeRefs';
import usePressEvents from '../../modules/usePressEvents';
import StyleSheet from '../StyleSheet';
import View from '../View';
//import { warnOnce } from '../../modules/warnOnce';
type ViewStyle = $PropertyType<ViewProps, 'style'>;
type Props = $ReadOnly<{|
...TouchableWithoutFeedbackProps,
activeOpacity?: ?number,
onHideUnderlay?: ?() => void,
onShowUnderlay?: ?() => void,
style?: ViewStyle,
testOnly_pressed?: ?boolean,
underlayColor?: ?ColorValue
|}>;
type ExtraStyles = $ReadOnly<{|
child: ViewStyle,
underlay: ViewStyle
|}>;
function createExtraStyles(activeOpacity, underlayColor): ExtraStyles {
return {
child: { opacity: activeOpacity ?? 0.85 },
underlay: {
backgroundColor: underlayColor === undefined ? 'black' : underlayColor
}
};
}
function hasPressHandler(props): boolean {
return (
props.onPress != null ||
props.onPressIn != null ||
props.onPressOut != null ||
props.onLongPress != null
);
}
/**
* 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.
*/
function TouchableHighlight(props: Props, forwardedRef): React.Node {
/*
warnOnce(
'TouchableHighlight',
'TouchableHighlight is deprecated. Please use Pressable.'
);
*/
const {
activeOpacity,
children,
delayPressIn,
delayPressOut,
delayLongPress,
disabled,
focusable,
onHideUnderlay,
onLongPress,
onPress,
onPressIn,
onPressOut,
onShowUnderlay,
rejectResponderTermination,
style,
testOnly_pressed,
underlayColor,
...rest
} = props;
const hostRef = useRef(null);
const setRef = useMergeRefs(forwardedRef, hostRef);
const [extraStyles, setExtraStyles] = useState(
testOnly_pressed === true
? createExtraStyles(activeOpacity, underlayColor)
: null
);
const showUnderlay = useCallback(() => {
if (!hasPressHandler(props)) {
return;
}
setExtraStyles(createExtraStyles(activeOpacity, underlayColor));
if (onShowUnderlay != null) {
onShowUnderlay();
}
}, [activeOpacity, onShowUnderlay, props, underlayColor]);
const hideUnderlay = useCallback(() => {
if (testOnly_pressed === true) {
return;
}
if (hasPressHandler(props)) {
setExtraStyles(null);
if (onHideUnderlay != null) {
onHideUnderlay();
}
}
}, [onHideUnderlay, props, testOnly_pressed]);
const pressConfig = useMemo(
() => ({
cancelable: !rejectResponderTermination,
disabled,
delayLongPress,
delayPressStart: delayPressIn,
delayPressEnd: delayPressOut,
onLongPress,
onPress,
onPressStart(event) {
showUnderlay();
if (onPressIn != null) {
onPressIn(event);
}
},
onPressEnd(event) {
hideUnderlay();
if (onPressOut != null) {
onPressOut(event);
}
}
}),
[
delayLongPress,
delayPressIn,
delayPressOut,
disabled,
onLongPress,
onPress,
onPressIn,
onPressOut,
rejectResponderTermination,
showUnderlay,
hideUnderlay
]
);
const pressEventHandlers = usePressEvents(hostRef, pressConfig);
const child = React.Children.only(children);
return (
<View
{...rest}
{...pressEventHandlers}
accessibilityDisabled={disabled}
focusable={!disabled && focusable !== false}
pointerEvents={disabled ? 'box-none' : undefined}
ref={setRef}
style={[
styles.root,
style,
!disabled && styles.actionable,
extraStyles && extraStyles.underlay
]}
>
{React.cloneElement(child, {
style: [child.props.style, extraStyles && extraStyles.child]
})}
</View>
);
}
const styles = StyleSheet.create({
root: {
userSelect: 'none'
},
actionable: {
cursor: 'pointer',
touchAction: 'manipulation'
}
});
const MemoedTouchableHighlight = React.memo(
React.forwardRef(TouchableHighlight)
);
MemoedTouchableHighlight.displayName = 'TouchableHighlight';
export default (MemoedTouchableHighlight: React.AbstractComponent<
Props,
React.ElementRef<typeof View>
>);
@@ -0,0 +1,11 @@
/**
* Copyright (c) Nicolas Gallagher.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import UnimplementedView from '../../modules/UnimplementedView';
export default UnimplementedView;
@@ -0,0 +1,168 @@
/**
* 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 client';
import type { Props as TouchableWithoutFeedbackProps } from '../TouchableWithoutFeedback';
import type { ViewProps } from '../View';
import * as React from 'react';
import { useCallback, useMemo, useState, useRef } from 'react';
import useMergeRefs from '../../modules/useMergeRefs';
import usePressEvents from '../../modules/usePressEvents';
import StyleSheet from '../StyleSheet';
import View from '../View';
//import { warnOnce } from '../../modules/warnOnce';
type ViewStyle = $PropertyType<ViewProps, 'style'>;
type Props = $ReadOnly<{|
...TouchableWithoutFeedbackProps,
activeOpacity?: ?number,
style?: ?ViewStyle
|}>;
/**
* A wrapper for making views respond properly to touches.
* On press down, the opacity of the wrapped view is decreased, dimming it.
*/
function TouchableOpacity(props: Props, forwardedRef): React.Node {
/*
warnOnce(
'TouchableOpacity',
'TouchableOpacity is deprecated. Please use Pressable.'
);
*/
const {
activeOpacity,
delayPressIn,
delayPressOut,
delayLongPress,
disabled,
focusable,
onLongPress,
onPress,
onPressIn,
onPressOut,
rejectResponderTermination,
style,
...rest
} = props;
const hostRef = useRef(null);
const setRef = useMergeRefs(forwardedRef, hostRef);
const [duration, setDuration] = useState('0s');
const [opacityOverride, setOpacityOverride] = useState(null);
const setOpacityTo = useCallback(
(value: ?number, duration: number) => {
setOpacityOverride(value);
setDuration(duration ? `${duration / 1000}s` : '0s');
},
[setOpacityOverride, setDuration]
);
const setOpacityActive = useCallback(
(duration: number) => {
setOpacityTo(activeOpacity ?? 0.2, duration);
},
[activeOpacity, setOpacityTo]
);
const setOpacityInactive = useCallback(
(duration: number) => {
setOpacityTo(null, duration);
},
[setOpacityTo]
);
const pressConfig = useMemo(
() => ({
cancelable: !rejectResponderTermination,
disabled,
delayLongPress,
delayPressStart: delayPressIn,
delayPressEnd: delayPressOut,
onLongPress,
onPress,
onPressStart(event) {
const isGrant =
event.dispatchConfig != null
? event.dispatchConfig.registrationName === 'onResponderGrant'
: event.type === 'keydown';
setOpacityActive(isGrant ? 0 : 150);
if (onPressIn != null) {
onPressIn(event);
}
},
onPressEnd(event) {
setOpacityInactive(250);
if (onPressOut != null) {
onPressOut(event);
}
}
}),
[
delayLongPress,
delayPressIn,
delayPressOut,
disabled,
onLongPress,
onPress,
onPressIn,
onPressOut,
rejectResponderTermination,
setOpacityActive,
setOpacityInactive
]
);
const pressEventHandlers = usePressEvents(hostRef, pressConfig);
return (
<View
{...rest}
{...pressEventHandlers}
accessibilityDisabled={disabled}
focusable={!disabled && focusable !== false}
pointerEvents={disabled ? 'box-none' : undefined}
ref={setRef}
style={[
styles.root,
!disabled && styles.actionable,
style,
opacityOverride != null && { opacity: opacityOverride },
{ transitionDuration: duration }
]}
/>
);
}
const styles = StyleSheet.create({
root: {
transitionProperty: 'opacity',
transitionDuration: '0.15s',
userSelect: 'none'
},
actionable: {
cursor: 'pointer',
touchAction: 'manipulation'
}
});
const MemoedTouchableOpacity = React.memo(React.forwardRef(TouchableOpacity));
MemoedTouchableOpacity.displayName = 'TouchableOpacity';
export default (MemoedTouchableOpacity: React.AbstractComponent<
Props,
React.ElementRef<typeof View>
>);
@@ -0,0 +1,132 @@
/**
* 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 client';
import type { PressResponderConfig } from '../../modules/usePressEvents/PressResponder';
import type { ViewProps } from '../View';
import * as React from 'react';
import { useMemo, useRef } from 'react';
import pick from '../../modules/pick';
import useMergeRefs from '../../modules/useMergeRefs';
import usePressEvents from '../../modules/usePressEvents';
import { warnOnce } from '../../modules/warnOnce';
export type Props = $ReadOnly<{|
accessibilityLabel?: $PropertyType<ViewProps, 'accessibilityLabel'>,
accessibilityLiveRegion?: $PropertyType<ViewProps, 'accessibilityLiveRegion'>,
accessibilityRole?: $PropertyType<ViewProps, 'accessibilityRole'>,
children?: ?React.Node,
delayLongPress?: ?number,
delayPressIn?: ?number,
delayPressOut?: ?number,
disabled?: ?boolean,
focusable?: ?boolean,
nativeID?: $PropertyType<ViewProps, 'nativeID'>,
onBlur?: $PropertyType<ViewProps, 'onBlur'>,
onFocus?: $PropertyType<ViewProps, 'onFocus'>,
onLayout?: $PropertyType<ViewProps, 'onLayout'>,
onLongPress?: $PropertyType<PressResponderConfig, 'onLongPress'>,
onPress?: $PropertyType<PressResponderConfig, 'onPress'>,
onPressIn?: $PropertyType<PressResponderConfig, 'onPressStart'>,
onPressOut?: $PropertyType<PressResponderConfig, 'onPressEnd'>,
rejectResponderTermination?: ?boolean,
testID?: $PropertyType<ViewProps, 'testID'>
|}>;
const forwardPropsList = {
accessibilityDisabled: true,
accessibilityLabel: true,
accessibilityLiveRegion: true,
accessibilityRole: true,
accessibilityState: true,
accessibilityValue: true,
children: true,
disabled: true,
focusable: true,
nativeID: true,
onBlur: true,
onFocus: true,
onLayout: true,
testID: true
};
const pickProps = (props) => pick(props, forwardPropsList);
function TouchableWithoutFeedback(props: Props, forwardedRef): React.Node {
warnOnce(
'TouchableWithoutFeedback',
'TouchableWithoutFeedback is deprecated. Please use Pressable.'
);
const {
delayPressIn,
delayPressOut,
delayLongPress,
disabled,
focusable,
onLongPress,
onPress,
onPressIn,
onPressOut,
rejectResponderTermination
} = props;
const hostRef = useRef(null);
const pressConfig = useMemo(
() => ({
cancelable: !rejectResponderTermination,
disabled,
delayLongPress,
delayPressStart: delayPressIn,
delayPressEnd: delayPressOut,
onLongPress,
onPress,
onPressStart: onPressIn,
onPressEnd: onPressOut
}),
[
disabled,
delayPressIn,
delayPressOut,
delayLongPress,
onLongPress,
onPress,
onPressIn,
onPressOut,
rejectResponderTermination
]
);
const pressEventHandlers = usePressEvents(hostRef, pressConfig);
const element = React.Children.only(props.children);
const children = [element.props.children];
const supportedProps = pickProps(props);
supportedProps.accessibilityDisabled = disabled;
supportedProps.focusable = !disabled && focusable !== false;
supportedProps.ref = useMergeRefs(forwardedRef, hostRef, element.ref);
const elementProps = Object.assign(supportedProps, pressEventHandlers);
return React.cloneElement(element, elementProps, ...children);
}
const MemoedTouchableWithoutFeedback = React.memo(
React.forwardRef(TouchableWithoutFeedback)
);
MemoedTouchableWithoutFeedback.displayName = 'TouchableWithoutFeedback';
export default (MemoedTouchableWithoutFeedback: React.AbstractComponent<
Props,
React.ElementRef<any>
>);
@@ -0,0 +1,132 @@
/**
* Copyright (c) Nicolas Gallagher.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @noflow
*/
import getBoundingClientRect from '../../modules/getBoundingClientRect';
import setValueForStyles from '../../modules/setValueForStyles';
const getRect = (node) => {
const height = node.offsetHeight;
const width = node.offsetWidth;
let left = node.offsetLeft;
let top = node.offsetTop;
node = node.offsetParent;
while (node && node.nodeType === 1 /* Node.ELEMENT_NODE */) {
left += node.offsetLeft + node.clientLeft - node.scrollLeft;
top += node.offsetTop + node.clientTop - node.scrollTop;
node = node.offsetParent;
}
top -= window.scrollY;
left -= window.scrollX;
return { width, height, top, left };
};
const measureLayout = (node, relativeToNativeNode, callback) => {
const relativeNode = relativeToNativeNode || (node && node.parentNode);
if (node && relativeNode) {
setTimeout(() => {
if (node.isConnected && relativeNode.isConnected) {
const relativeRect = getRect(relativeNode);
const { height, left, top, width } = getRect(node);
const x = left - relativeRect.left;
const y = top - relativeRect.top;
callback(x, y, width, height, left, top);
}
}, 0);
}
};
const elementsToIgnore = {
A: true,
BODY: true,
INPUT: true,
SELECT: true,
TEXTAREA: true
};
const UIManager = {
blur(node) {
try {
node.blur();
} catch (err) {}
},
focus(node) {
try {
const name = node.nodeName;
// A tabIndex of -1 allows element to be programmatically focused but
// prevents keyboard focus. We don't want to set the tabindex value on
// elements that should not prevent keyboard focus.
if (
node.getAttribute('tabIndex') == null &&
node.isContentEditable !== true &&
elementsToIgnore[name] == null
) {
node.setAttribute('tabIndex', '-1');
}
node.focus();
} catch (err) {}
},
measure(node, callback) {
measureLayout(node, null, callback);
},
measureInWindow(node, callback) {
if (node) {
setTimeout(() => {
const { height, left, top, width } = getBoundingClientRect(node);
callback(left, top, width, height);
}, 0);
}
},
measureLayout(node, relativeToNativeNode, onFail, onSuccess) {
measureLayout(node, relativeToNativeNode, onSuccess);
},
updateView(node, props) {
for (const prop in props) {
if (!Object.prototype.hasOwnProperty.call(props, prop)) {
continue;
}
const value = props[prop];
switch (prop) {
case 'style': {
setValueForStyles(node, value);
break;
}
case 'class':
case 'className': {
node.setAttribute('class', value);
break;
}
case 'text':
case 'value':
// native platforms use `text` prop to replace text input value
node.value = value;
break;
default:
node.setAttribute(prop, value);
}
}
},
configureNextLayoutAnimation(config, onAnimationDidEnd) {
onAnimationDidEnd();
},
// mocks
setLayoutAnimationEnabledExperimental() {}
};
export default UIManager;
@@ -0,0 +1,28 @@
/**
* Copyright (c) Nicolas Gallagher.
* 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
*/
type VibratePattern = number | Array<number>;
const vibrate = (pattern: VibratePattern) => {
if ('vibrate' in window.navigator) {
window.navigator.vibrate(pattern);
}
};
const Vibration = {
cancel() {
vibrate(0);
},
vibrate(pattern: VibratePattern = 400) {
vibrate(pattern);
}
};
export default Vibration;
+175
View File
@@ -0,0 +1,175 @@
/**
* Copyright (c) Nicolas Gallagher.
* 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
*/
'use client';
import type { PlatformMethods } from '../../types';
import type { ViewProps } from './types';
import * as React from 'react';
import createElement from '../createElement';
import * as forwardedProps from '../../modules/forwardedProps';
import pick from '../../modules/pick';
import useElementLayout from '../../modules/useElementLayout';
import useMergeRefs from '../../modules/useMergeRefs';
import usePlatformMethods from '../../modules/usePlatformMethods';
import useResponderEvents from '../../modules/useResponderEvents';
import StyleSheet from '../StyleSheet';
import TextAncestorContext from '../Text/TextAncestorContext';
import { useLocaleContext, getLocaleDirection } from '../../modules/useLocale';
const forwardPropsList = Object.assign(
{},
forwardedProps.defaultProps,
forwardedProps.accessibilityProps,
forwardedProps.clickProps,
forwardedProps.focusProps,
forwardedProps.keyboardProps,
forwardedProps.mouseProps,
forwardedProps.touchProps,
forwardedProps.styleProps,
{
href: true,
lang: true,
onScroll: true,
onWheel: true,
pointerEvents: true
}
);
const pickProps = (props) => pick(props, forwardPropsList);
const View: React.AbstractComponent<ViewProps, HTMLElement & PlatformMethods> =
React.forwardRef((props, forwardedRef) => {
const {
hrefAttrs,
onLayout,
onMoveShouldSetResponder,
onMoveShouldSetResponderCapture,
onResponderEnd,
onResponderGrant,
onResponderMove,
onResponderReject,
onResponderRelease,
onResponderStart,
onResponderTerminate,
onResponderTerminationRequest,
onScrollShouldSetResponder,
onScrollShouldSetResponderCapture,
onSelectionChangeShouldSetResponder,
onSelectionChangeShouldSetResponderCapture,
onStartShouldSetResponder,
onStartShouldSetResponderCapture,
...rest
} = props;
if (process.env.NODE_ENV !== 'production') {
React.Children.toArray(props.children).forEach((item) => {
if (typeof item === 'string') {
console.error(
`Unexpected text node: ${item}. A text node cannot be a child of a <View>.`
);
}
});
}
const hasTextAncestor = React.useContext(TextAncestorContext);
const hostRef = React.useRef(null);
const { direction: contextDirection } = useLocaleContext();
useElementLayout(hostRef, onLayout);
useResponderEvents(hostRef, {
onMoveShouldSetResponder,
onMoveShouldSetResponderCapture,
onResponderEnd,
onResponderGrant,
onResponderMove,
onResponderReject,
onResponderRelease,
onResponderStart,
onResponderTerminate,
onResponderTerminationRequest,
onScrollShouldSetResponder,
onScrollShouldSetResponderCapture,
onSelectionChangeShouldSetResponder,
onSelectionChangeShouldSetResponderCapture,
onStartShouldSetResponder,
onStartShouldSetResponderCapture
});
let component = 'div';
const langDirection =
props.lang != null ? getLocaleDirection(props.lang) : null;
const componentDirection = props.dir || langDirection;
const writingDirection = componentDirection || contextDirection;
const supportedProps = pickProps(rest);
supportedProps.dir = componentDirection;
supportedProps.style = [
styles.view$raw,
hasTextAncestor && styles.inline,
props.style
];
if (props.href != null) {
component = 'a';
if (hrefAttrs != null) {
const { download, rel, target } = hrefAttrs;
if (download != null) {
supportedProps.download = download;
}
if (rel != null) {
supportedProps.rel = rel;
}
if (typeof target === 'string') {
supportedProps.target =
target.charAt(0) !== '_' ? '_' + target : target;
}
}
}
const platformMethodsRef = usePlatformMethods(supportedProps);
const setRef = useMergeRefs(hostRef, platformMethodsRef, forwardedRef);
supportedProps.ref = setRef;
return createElement(component, supportedProps, { writingDirection });
});
View.displayName = 'View';
const styles = StyleSheet.create({
view$raw: {
alignContent: 'flex-start',
alignItems: 'stretch',
backgroundColor: 'transparent',
border: '0 solid black',
boxSizing: 'border-box',
display: 'flex',
flexBasis: 'auto',
flexDirection: 'column',
flexShrink: 0,
listStyle: 'none',
margin: 0,
minHeight: 0,
minWidth: 0,
padding: 0,
position: 'relative',
textDecoration: 'none',
zIndex: 0
},
inline: {
display: 'inline-flex'
}
});
export type { ViewProps };
export default View;
+247
View File
@@ -0,0 +1,247 @@
/**
* Copyright (c) Nicolas Gallagher.
* 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
*/
import type { ColorValue, GenericStyleProp, LayoutEvent } from '../../types';
import type {
AnimationStyles,
BorderStyles,
InteractionStyles,
LayoutStyles,
ShadowStyles,
TransformStyles
} from '../../types/styles';
type NumberOrString = number | string;
type OverscrollBehaviorValue = 'auto' | 'contain' | 'none';
type idRef = string;
type idRefList = idRef | Array<idRef>;
export type AccessibilityProps = {|
'aria-activedescendant'?: ?idRef,
'aria-atomic'?: ?boolean,
'aria-autocomplete'?: ?('none' | 'list' | 'inline' | 'both'),
'aria-busy'?: ?boolean,
'aria-checked'?: ?(boolean | 'mixed'),
'aria-colcount'?: ?number,
'aria-colindex'?: ?number,
'aria-colspan'?: ?number,
'aria-controls'?: ?idRef,
'aria-current'?: ?(boolean | 'page' | 'step' | 'location' | 'date' | 'time'),
'aria-describedby'?: ?idRef,
'aria-details'?: ?idRef,
'aria-disabled'?: ?boolean,
'aria-errormessage'?: ?idRef,
'aria-expanded'?: ?boolean,
'aria-flowto'?: ?idRef,
'aria-haspopup'?: ?('dialog' | 'grid' | 'listbox' | 'menu' | 'tree' | false),
'aria-hidden'?: ?boolean,
'aria-invalid'?: ?boolean,
'aria-keyshortcuts'?: ?Array<string>,
'aria-label'?: ?string,
'aria-labelledby'?: ?idRef,
'aria-level'?: ?number,
'aria-live'?: ?('assertive' | 'none' | 'polite'),
'aria-modal'?: ?boolean,
'aria-multiline'?: ?boolean,
'aria-multiselectable'?: ?boolean,
'aria-orientation'?: ?('horizontal' | 'vertical'),
'aria-owns'?: ?idRef,
'aria-placeholder'?: ?string,
'aria-posinset'?: ?number,
'aria-pressed'?: ?(boolean | 'mixed'),
'aria-readonly'?: ?boolean,
'aria-required'?: ?boolean,
'aria-roledescription'?: ?string,
'aria-rowcount'?: ?number,
'aria-rowindex'?: ?number,
'aria-rowspan'?: ?number,
'aria-selected'?: ?boolean,
'aria-setsize'?: ?number,
'aria-sort'?: ?('ascending' | 'descending' | 'none' | 'other'),
'aria-valuemax'?: ?number,
'aria-valuemin'?: ?number,
'aria-valuenow'?: ?number,
'aria-valuetext'?: ?string,
role?: ?string,
// @deprecated
accessibilityActiveDescendant?: ?idRef,
accessibilityAtomic?: ?boolean,
accessibilityAutoComplete?: ?('none' | 'list' | 'inline' | 'both'),
accessibilityBusy?: ?boolean,
accessibilityChecked?: ?(boolean | 'mixed'),
accessibilityColumnCount?: ?number,
accessibilityColumnIndex?: ?number,
accessibilityColumnSpan?: ?number,
accessibilityControls?: ?idRefList,
accessibilityCurrent?: ?(
| boolean
| 'page'
| 'step'
| 'location'
| 'date'
| 'time'
),
accessibilityDescribedBy?: ?idRefList,
accessibilityDetails?: ?idRef,
accessibilityDisabled?: ?boolean,
accessibilityErrorMessage?: ?idRef,
accessibilityExpanded?: ?boolean,
accessibilityFlowTo?: ?idRefList,
accessibilityHasPopup?: ?(
| 'dialog'
| 'grid'
| 'listbox'
| 'menu'
| 'tree'
| false
),
accessibilityHidden?: ?boolean,
accessibilityInvalid?: ?boolean,
accessibilityKeyShortcuts?: ?Array<string>,
accessibilityLabel?: ?string,
accessibilityLabelledBy?: ?idRefList,
accessibilityLevel?: ?number,
accessibilityLiveRegion?: ?('assertive' | 'none' | 'polite'),
accessibilityModal?: ?boolean,
accessibilityMultiline?: ?boolean,
accessibilityMultiSelectable?: ?boolean,
accessibilityOrientation?: ?('horizontal' | 'vertical'),
accessibilityOwns?: ?idRefList,
accessibilityPlaceholder?: ?string,
accessibilityPosInSet?: ?number,
accessibilityPressed?: ?(boolean | 'mixed'),
accessibilityReadOnly?: ?boolean,
accessibilityRequired?: ?boolean,
accessibilityRole?: ?string,
accessibilityRoleDescription?: ?string,
accessibilityRowCount?: ?number,
accessibilityRowIndex?: ?number,
accessibilityRowSpan?: ?number,
accessibilitySelected?: ?boolean,
accessibilitySetSize?: ?number,
accessibilitySort?: ?('ascending' | 'descending' | 'none' | 'other'),
accessibilityValueMax?: ?number,
accessibilityValueMin?: ?number,
accessibilityValueNow?: ?number,
accessibilityValueText?: ?string
|};
export type EventProps = {|
onAuxClick?: (e: any) => void,
onBlur?: (e: any) => void,
onClick?: (e: any) => void,
onContextMenu?: (e: any) => void,
onFocus?: (e: any) => void,
onGotPointerCapture?: (e: any) => void,
onKeyDown?: (e: any) => void,
onKeyUp?: (e: any) => void,
onLayout?: (e: LayoutEvent) => void,
onLostPointerCapture?: (e: any) => void,
onMoveShouldSetResponder?: (e: any) => boolean,
onMoveShouldSetResponderCapture?: (e: any) => boolean,
onPointerCancel?: (e: any) => void,
onPointerDown?: (e: any) => void,
onPointerEnter?: (e: any) => void,
onPointerMove?: (e: any) => void,
onPointerLeave?: (e: any) => void,
onPointerOut?: (e: any) => void,
onPointerOver?: (e: any) => void,
onPointerUp?: (e: any) => void,
onResponderEnd?: (e: any) => void,
onResponderGrant?: (e: any) => void | boolean,
onResponderMove?: (e: any) => void,
onResponderReject?: (e: any) => void,
onResponderRelease?: (e: any) => void,
onResponderStart?: (e: any) => void,
onResponderTerminate?: (e: any) => void,
onResponderTerminationRequest?: (e: any) => boolean,
onScrollShouldSetResponder?: (e: any) => boolean,
onScrollShouldSetResponderCapture?: (e: any) => boolean,
onSelectionChangeShouldSetResponder?: (e: any) => boolean,
onSelectionChangeShouldSetResponderCapture?: (e: any) => boolean,
onStartShouldSetResponder?: (e: any) => boolean,
onStartShouldSetResponderCapture?: (e: any) => boolean,
// unstable
onMouseDown?: (e: any) => void,
onMouseEnter?: (e: any) => void,
onMouseLeave?: (e: any) => void,
onMouseMove?: (e: any) => void,
onMouseOver?: (e: any) => void,
onMouseOut?: (e: any) => void,
onMouseUp?: (e: any) => void,
onScroll?: (e: any) => void,
onTouchCancel?: (e: any) => void,
onTouchCancelCapture?: (e: any) => void,
onTouchEnd?: (e: any) => void,
onTouchEndCapture?: (e: any) => void,
onTouchMove?: (e: any) => void,
onTouchMoveCapture?: (e: any) => void,
onTouchStart?: (e: any) => void,
onTouchStartCapture?: (e: any) => void,
onWheel?: (e: any) => void
|};
export type ViewStyle = {
...AnimationStyles,
...BorderStyles,
...InteractionStyles,
...LayoutStyles,
...ShadowStyles,
...TransformStyles,
backdropFilter?: ?string,
backgroundAttachment?: ?string,
backgroundBlendMode?: ?string,
backgroundClip?: ?string,
backgroundColor?: ?ColorValue,
backgroundImage?: ?string,
backgroundOrigin?: 'border-box' | 'content-box' | 'padding-box',
backgroundPosition?: ?string,
backgroundRepeat?: ?string,
backgroundSize?: ?string,
boxShadow?: ?string,
clip?: ?string,
filter?: ?string,
opacity?: ?number,
outlineColor?: ?ColorValue,
outlineOffset?: ?NumberOrString,
outlineStyle?: ?string,
outlineWidth?: ?NumberOrString,
overscrollBehavior?: ?OverscrollBehaviorValue,
overscrollBehaviorX?: ?OverscrollBehaviorValue,
overscrollBehaviorY?: ?OverscrollBehaviorValue,
pointerEvents?: 'box-none' | 'none' | 'box-only' | 'auto',
scrollbarWidth?: 'auto' | 'none' | 'thin',
scrollSnapAlign?: ?string,
scrollSnapType?: ?string,
WebkitMaskImage?: ?string,
WebkitOverflowScrolling?: 'auto' | 'touch'
};
export type ViewProps = {
...AccessibilityProps,
...EventProps,
children?: ?any,
dataSet?: { ... },
dir?: 'ltr' | 'rtl',
id?: ?string,
lang?: string,
style?: GenericStyleProp<ViewStyle>,
tabIndex?: ?(0 | -1),
testID?: ?string,
// unstable
href?: ?string,
hrefAttrs?: ?{ download?: ?boolean, rel?: ?string, target?: ?string },
// @deprecated
focusable?: ?boolean,
pointerEvents?: 'box-none' | 'none' | 'box-only' | 'auto',
nativeID?: ?string
};
@@ -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
*/
'use client';
import VirtualizedList from '../../vendor/react-native/VirtualizedList';
export default VirtualizedList;
@@ -0,0 +1,22 @@
/**
* Copyright (c) Nicolas Gallagher.
* 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
*/
import type { Node } from 'React';
import React from 'react';
import UnimplementedView from '../../modules/UnimplementedView';
function YellowBox(props: Object): Node {
return <UnimplementedView {...props} />;
}
YellowBox.ignoreWarnings = () => {};
export default YellowBox;
@@ -0,0 +1,43 @@
/**
* Copyright (c) Nicolas Gallagher.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @noflow
*/
'use client';
import AccessibilityUtil from '../../modules/AccessibilityUtil';
import createDOMProps from '../../modules/createDOMProps';
import React from 'react';
import { LocaleProvider } from '../../modules/useLocale';
const createElement = (component, props, options) => {
// Use equivalent platform elements where possible.
let accessibilityComponent;
if (component && component.constructor === String) {
accessibilityComponent =
AccessibilityUtil.propsToAccessibilityComponent(props);
}
const Component = accessibilityComponent || component;
const domProps = createDOMProps(Component, props, options);
const element = React.createElement(Component, domProps);
// Update locale context if element's writing direction prop changes
const elementWithLocaleProvider = domProps.dir ? (
<LocaleProvider
children={element}
direction={domProps.dir}
locale={domProps.lang}
/>
) : (
element
);
return elementWithLocaleProvider;
};
export default createElement;
@@ -0,0 +1,18 @@
/**
* Copyright (c) Nicolas Gallagher.
* 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.
*
* @noflow
*/
const findNodeHandle = (component) => {
throw new Error(
'findNodeHandle is not supported on web. ' +
'Use the ref property on the component instead.'
);
};
export default findNodeHandle;
@@ -0,0 +1,29 @@
/**
* Copyright (c) Nicolas Gallagher.
* 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
*/
import normalizeColor from '@react-native/normalize-colors';
const processColor = (color?: string | number): ?number => {
if (color === undefined || color === null) {
return color;
}
// convert number and hex
let int32Color = normalizeColor(color);
if (int32Color === undefined || int32Color === null) {
return undefined;
}
int32Color = ((int32Color << 24) | (int32Color >>> 8)) >>> 0;
return int32Color;
};
export default processColor;
@@ -0,0 +1,29 @@
/**
* Copyright (c) Nicolas Gallagher.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @noflow
*/
'use client';
import {
createRoot as domCreateRoot,
hydrateRoot as domHydrateRoot
} from 'react-dom/client';
import { createSheet } from '../StyleSheet/dom';
export function hydrate(element, root) {
createSheet(root);
return domHydrateRoot(root, element);
}
export default function render(element, root) {
createSheet(root);
const reactRoot = domCreateRoot(root);
reactRoot.render(element);
return reactRoot;
}
@@ -0,0 +1,13 @@
/**
* Copyright (c) Nicolas Gallagher.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @noflow
*/
export default function unmountComponentAtNode(rootTag) {
rootTag.unmount();
return true;
}
@@ -0,0 +1,31 @@
/**
* Copyright (c) Nicolas Gallagher.
* 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
*/
'use client';
import * as React from 'react';
import type { ColorSchemeName } from '../Appearance';
import Appearance from '../Appearance';
export default function useColorScheme(): ColorSchemeName {
const [colorScheme, setColorScheme] = React.useState(
Appearance.getColorScheme()
);
React.useEffect(() => {
function listener(appearance) {
setColorScheme(appearance.colorScheme);
}
const { remove } = Appearance.addChangeListener(listener);
return remove;
});
return colorScheme;
}
@@ -0,0 +1,13 @@
/**
* Copyright (c) Nicolas Gallagher.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict
*/
'use client';
import { useLocaleContext } from '../../modules/useLocale';
export default useLocaleContext;
@@ -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.
*
* @format
* @flow strict-local
*/
'use client';
import type { DisplayMetrics } from '../Dimensions';
import Dimensions from '../Dimensions';
import { useEffect, useState } from 'react';
export default function useWindowDimensions(): DisplayMetrics {
const [dims, setDims] = useState(() => Dimensions.get('window'));
useEffect(() => {
function handleChange({ window }) {
if (window != null) {
setDims(window);
}
}
Dimensions.addEventListener('change', handleChange);
// We might have missed an update between calling `get` in render and
// `addEventListener` in this handler, so we set it here. If there was
// no change, React will filter out this update as a no-op.
setDims(Dimensions.get('window'));
return () => {
Dimensions.removeEventListener('change', handleChange);
};
}, []);
return dims;
}
+70
View File
@@ -0,0 +1,70 @@
// @flow strict
export { default as unstable_createElement } from './exports/createElement';
export { default as findNodeHandle } from './exports/findNodeHandle';
export { default as processColor } from './exports/processColor';
export { default as render } from './exports/render';
export { default as unmountComponentAtNode } from './exports/unmountComponentAtNode';
export { default as NativeModules } from './exports/NativeModules';
// APIs
export { default as AccessibilityInfo } from './exports/AccessibilityInfo';
export { default as Alert } from './exports/Alert';
export { default as Animated } from './exports/Animated';
export { default as Appearance } from './exports/Appearance';
export { default as AppRegistry } from './exports/AppRegistry';
export { default as AppState } from './exports/AppState';
export { default as BackHandler } from './exports/BackHandler';
export { default as Clipboard } from './exports/Clipboard';
export { default as Dimensions } from './exports/Dimensions';
export { default as Easing } from './exports/Easing';
export { default as I18nManager } from './exports/I18nManager';
export { default as Keyboard } from './exports/Keyboard';
export { default as InteractionManager } from './exports/InteractionManager';
export { default as LayoutAnimation } from './exports/LayoutAnimation';
export { default as Linking } from './exports/Linking';
export { default as NativeEventEmitter } from './exports/NativeEventEmitter';
export { default as PanResponder } from './exports/PanResponder';
export { default as PixelRatio } from './exports/PixelRatio';
export { default as Platform } from './exports/Platform';
export { default as Share } from './exports/Share';
export { default as StyleSheet } from './exports/StyleSheet';
export { default as UIManager } from './exports/UIManager';
export { default as Vibration } from './exports/Vibration';
// components
export { default as ActivityIndicator } from './exports/ActivityIndicator';
export { default as Button } from './exports/Button';
export { default as CheckBox } from './exports/CheckBox';
export { default as FlatList } from './exports/FlatList';
export { default as Image } from './exports/Image';
export { default as ImageBackground } from './exports/ImageBackground';
export { default as KeyboardAvoidingView } from './exports/KeyboardAvoidingView';
export { default as Modal } from './exports/Modal';
export { default as Picker } from './exports/Picker';
export { default as Pressable } from './exports/Pressable';
export { default as ProgressBar } from './exports/ProgressBar';
export { default as RefreshControl } from './exports/RefreshControl';
export { default as SafeAreaView } from './exports/SafeAreaView';
export { default as ScrollView } from './exports/ScrollView';
export { default as SectionList } from './exports/SectionList';
export { default as StatusBar } from './exports/StatusBar';
export { default as Switch } from './exports/Switch';
export { default as Text } from './exports/Text';
export { default as TextInput } from './exports/TextInput';
export { default as Touchable } from './exports/Touchable';
export { default as TouchableHighlight } from './exports/TouchableHighlight';
export { default as TouchableNativeFeedback } from './exports/TouchableNativeFeedback';
export { default as TouchableOpacity } from './exports/TouchableOpacity';
export { default as TouchableWithoutFeedback } from './exports/TouchableWithoutFeedback';
export { default as View } from './exports/View';
export { default as VirtualizedList } from './exports/VirtualizedList';
export { default as YellowBox } from './exports/YellowBox';
export { default as LogBox } from './exports/LogBox';
// plugins
export { default as DeviceEventEmitter } from './exports/DeviceEventEmitter';
// hooks
export { default as useColorScheme } from './exports/useColorScheme';
export { default as useLocaleContext } from './exports/useLocaleContext';
export { default as useWindowDimensions } from './exports/useWindowDimensions';
@@ -0,0 +1,20 @@
/**
* Copyright (c) Nicolas Gallagher.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import isDisabled from './isDisabled';
import propsToAccessibilityComponent from './propsToAccessibilityComponent';
import propsToAriaRole from './propsToAriaRole';
const AccessibilityUtil = {
isDisabled,
propsToAccessibilityComponent,
propsToAriaRole
};
export default AccessibilityUtil;
@@ -0,0 +1,15 @@
/**
* Copyright (c) Nicolas Gallagher.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
const isDisabled = (props: Object): boolean =>
props.disabled ||
(Array.isArray(props.accessibilityStates) &&
props.accessibilityStates.indexOf('disabled') > -1);
export default isDisabled;
@@ -0,0 +1,58 @@
/**
* Copyright (c) Nicolas Gallagher.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import propsToAriaRole from './propsToAriaRole';
const roleComponents = {
article: 'article',
banner: 'header',
blockquote: 'blockquote',
button: 'button',
code: 'code',
complementary: 'aside',
contentinfo: 'footer',
deletion: 'del',
emphasis: 'em',
figure: 'figure',
insertion: 'ins',
form: 'form',
list: 'ul',
listitem: 'li',
main: 'main',
navigation: 'nav',
paragraph: 'p',
region: 'section',
strong: 'strong'
};
const emptyObject = {};
const propsToAccessibilityComponent = (
props: Object = emptyObject
): void | string => {
const roleProp = props.role || props.accessibilityRole;
// special-case for "label" role which doesn't map to an ARIA role
if (roleProp === 'label') {
return 'label';
}
const role = propsToAriaRole(props);
if (role) {
if (role === 'heading') {
const level = props.accessibilityLevel || props['aria-level'];
if (level != null) {
return `h${level}`;
}
return 'h1';
}
return roleComponents[role];
}
};
export default propsToAccessibilityComponent;
@@ -0,0 +1,42 @@
/**
* Copyright (c) Nicolas Gallagher.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
const accessibilityRoleToWebRole = {
adjustable: 'slider',
button: 'button',
header: 'heading',
image: 'img',
imagebutton: null,
keyboardkey: null,
label: null,
link: 'link',
none: 'presentation',
search: 'search',
summary: 'region',
text: null
};
const propsToAriaRole = ({
accessibilityRole,
role
}: {
accessibilityRole?: string,
role?: string
}): string | void => {
const _role = role || accessibilityRole;
if (_role) {
const inferredRole = accessibilityRoleToWebRole[_role];
if (inferredRole !== null) {
// ignore roles that don't map to web
return inferredRole || _role;
}
}
};
export default propsToAriaRole;
@@ -0,0 +1,32 @@
/**
* Copyright (c) Nicolas Gallagher.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
export type PackagerAsset = {
__packager_asset: boolean,
fileSystemLocation: string,
httpServerLocation: string,
width: ?number,
height: ?number,
scales: Array<number>,
hash: string,
name: string,
type: string
};
const assets: Array<PackagerAsset> = [];
export function registerAsset(asset: PackagerAsset): number {
// `push` returns new array length, so the first asset will
// get id 1 (not 0) to make the value truthy
return assets.push(asset);
}
export function getAssetByID(assetId: number): PackagerAsset {
return assets[assetId - 1];
}
@@ -0,0 +1,167 @@
/**
* Copyright (c) Nicolas Gallagher.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
const dataUriPattern = /^data:/;
export class ImageUriCache {
static _maximumEntries: number = 256;
static _entries = {};
static has(uri: string): boolean {
const entries = ImageUriCache._entries;
const isDataUri = dataUriPattern.test(uri);
return isDataUri || Boolean(entries[uri]);
}
static add(uri: string) {
const entries = ImageUriCache._entries;
const lastUsedTimestamp = Date.now();
if (entries[uri]) {
entries[uri].lastUsedTimestamp = lastUsedTimestamp;
entries[uri].refCount += 1;
} else {
entries[uri] = {
lastUsedTimestamp,
refCount: 1
};
}
}
static remove(uri: string) {
const entries = ImageUriCache._entries;
if (entries[uri]) {
entries[uri].refCount -= 1;
}
// Free up entries when the cache is "full"
ImageUriCache._cleanUpIfNeeded();
}
static _cleanUpIfNeeded() {
const entries = ImageUriCache._entries;
const imageUris = Object.keys(entries);
if (imageUris.length + 1 > ImageUriCache._maximumEntries) {
let leastRecentlyUsedKey;
let leastRecentlyUsedEntry;
imageUris.forEach((uri) => {
const entry = entries[uri];
if (
(!leastRecentlyUsedEntry ||
entry.lastUsedTimestamp <
leastRecentlyUsedEntry.lastUsedTimestamp) &&
entry.refCount === 0
) {
leastRecentlyUsedKey = uri;
leastRecentlyUsedEntry = entry;
}
});
if (leastRecentlyUsedKey) {
delete entries[leastRecentlyUsedKey];
}
}
}
}
let id = 0;
const requests = {};
const ImageLoader = {
abort(requestId: number) {
let image = requests[`${requestId}`];
if (image) {
image.onerror = null;
image.onload = null;
image = null;
delete requests[`${requestId}`];
}
},
getSize(
uri: string,
success: (width: number, height: number) => void,
failure: () => void
) {
let complete = false;
const interval = setInterval(callback, 16);
const requestId = ImageLoader.load(uri, callback, errorCallback);
function callback() {
const image = requests[`${requestId}`];
if (image) {
const { naturalHeight, naturalWidth } = image;
if (naturalHeight && naturalWidth) {
success(naturalWidth, naturalHeight);
complete = true;
}
}
if (complete) {
ImageLoader.abort(requestId);
clearInterval(interval);
}
}
function errorCallback() {
if (typeof failure === 'function') {
failure();
}
ImageLoader.abort(requestId);
clearInterval(interval);
}
},
has(uri: string): boolean {
return ImageUriCache.has(uri);
},
load(uri: string, onLoad: Function, onError: Function): number {
id += 1;
const image = new window.Image();
image.onerror = onError;
image.onload = (e) => {
// avoid blocking the main thread
const onDecode = () => onLoad({ nativeEvent: e });
if (typeof image.decode === 'function') {
// Safari currently throws exceptions when decoding svgs.
// We want to catch that error and allow the load handler
// to be forwarded to the onLoad handler in this case
image.decode().then(onDecode, onDecode);
} else {
setTimeout(onDecode, 0);
}
};
image.src = uri;
requests[`${id}`] = image;
return id;
},
prefetch(uri: string): Promise<void> {
return new Promise((resolve, reject) => {
ImageLoader.load(
uri,
() => {
// Add the uri to the cache so it can be immediately displayed when used
// but also immediately remove it to correctly reflect that it has no active references
ImageUriCache.add(uri);
ImageUriCache.remove(uri);
resolve();
},
reject
);
});
},
queryCache(uris: Array<string>): Promise<{| [uri: string]: 'disk/memory' |}> {
const result = {};
uris.forEach((u) => {
if (ImageUriCache.has(u)) {
result[u] = 'disk/memory';
}
});
return Promise.resolve(result);
}
};
export default ImageLoader;

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