Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import * as React from 'react';
import * as ReactNative from '../react-native';

import { errorMsg } from '../../shared/logUtils';
import { mergeRefs } from '../../shared/mergeRefs';
import { useNativeProps } from './useNativeProps';
import { useStrictDOMElement } from './useStrictDOMElement';

Expand All @@ -23,8 +24,22 @@ const AnimatedTextInput = ReactNative.Animated.createAnimatedComponent<
// $FlowFixMe: React Native animated component typing issue
>(ReactNative.TextInput);

// $FlowFixMe[unclear-type]
type Node = any;

type StrictInputProps = StrictReactDOMInputProps | StrictReactDOMTextAreaProps;

// Helper to update cached selection state for selectionStart/End polyfill
function updateCachedSelection(
node: ?Node,
selection: ?{ start: number, end: number }
) {
if (node != null && selection != null) {
node._selectionStart = selection.start;
node._selectionEnd = selection.end;
}
}

export function createStrictDOMTextInputComponent<P: StrictInputProps, T>(
tagName: string,
defaultProps?: P
Expand All @@ -33,6 +48,7 @@ export function createStrictDOMTextInputComponent<P: StrictInputProps, T>(
let NativeComponent:
| typeof ReactNative.TextInput
| typeof AnimatedTextInput = ReactNative.TextInput;
const nodeRef = React.useRef<?Node>(null);
const elementRef = useStrictDOMElement<T>(ref, { tagName });

const {
Expand All @@ -45,6 +61,7 @@ export function createStrictDOMTextInputComponent<P: StrictInputProps, T>(
onChange,
onInput,
onKeyDown,
onSelectionChange,
placeholder,
readOnly,
rows,
Expand Down Expand Up @@ -124,7 +141,9 @@ export function createStrictDOMTextInputComponent<P: StrictInputProps, T>(
}
if (onChange != null || onInput != null) {
nativeProps.onChange = function (e) {
const { text } = e.nativeEvent;
const { text, selection } = e.nativeEvent;
// Update cached selection state immediately to ensure sync with onChange
updateCachedSelection(nodeRef.current, selection);
if (onInput != null) {
onInput({
target: {
Expand Down Expand Up @@ -165,6 +184,14 @@ export function createStrictDOMTextInputComponent<P: StrictInputProps, T>(
});
};
}
// Part of polyfill for selectionStart/End
nativeProps.onSelectionChange = function (e) {
const { selection } = e.nativeEvent;
updateCachedSelection(nodeRef.current, selection);
if (onSelectionChange != null) {
onSelectionChange(e);
}
};
if (placeholder != null) {
nativeProps.placeholder = placeholder;
}
Expand All @@ -178,7 +205,13 @@ export function createStrictDOMTextInputComponent<P: StrictInputProps, T>(
nativeProps.value = value;
}

nativeProps.ref = elementRef;
nativeProps.ref = React.useMemo(
() =>
mergeRefs((node) => {
nodeRef.current = node;
}, elementRef),
[elementRef]
);

// Use Animated components if necessary
if (nativeProps.animated === true) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* @flow strict
*/

type Values = $ReadOnly<{
type MediaQueryValues = $ReadOnly<{
type?: 'screen',
width?: number,
height?: number,
Expand All @@ -18,5 +18,5 @@ type Values = $ReadOnly<{
}>;

declare export const mediaQuery : {
match: (mediaQuery: string, values: Values) => boolean
match: (mediaQuery: string, values: MediaQueryValues) => boolean
};
Original file line number Diff line number Diff line change
Expand Up @@ -11,29 +11,34 @@ import * as ReactNative from '../react-native';

import { useEffect, useState } from 'react';

const hasReducedMotionAPI =
ReactNative?.AccessibilityInfo?.isReduceMotionEnabled;

export function usePrefersReducedMotion(): boolean {
const [prefersReducedMotion, setPrefersReducedMotion] = useState(false);

useEffect(() => {
// 1. Get the initial value of reduce motion
ReactNative.AccessibilityInfo.isReduceMotionEnabled().then(
(isReduceMotionEnabled) => {
setPrefersReducedMotion(isReduceMotionEnabled);
}
);

// 2. Subscribe to changes in reduce motion
const reduceMotionChangedSubscription =
ReactNative.AccessibilityInfo.addEventListener(
'reduceMotionChanged',
if (hasReducedMotionAPI) {
// 1. Get the initial value of reduce motion
ReactNative.AccessibilityInfo.isReduceMotionEnabled().then(
(isReduceMotionEnabled) => {
setPrefersReducedMotion(isReduceMotionEnabled);
}
);

return () => {
reduceMotionChangedSubscription.remove();
};
// 2. Subscribe to changes in reduce motion
const reduceMotionChangedSubscription =
ReactNative.AccessibilityInfo.addEventListener(
'reduceMotionChanged',
(isReduceMotionEnabled) => {
setPrefersReducedMotion(isReduceMotionEnabled);
}
);

return () => {
reduceMotionChangedSubscription.remove();
};
}
}, []);

return prefersReducedMotion;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,39 @@ function getOrCreateStrictRef(
}
}
});
} else if (tagName === 'input' || tagName === 'textarea') {
const setSelectionRange = node?.setSelectionRange;
if (setSelectionRange == null) {
// $FlowFixMe[prop-missing]
Object.defineProperty(strictRef, 'setSelectionRange', {
value: (a: number, b: number) => {
node.setSelection(a, b);
// Update cached selection state
node._selectionStart = a;
node._selectionEnd = b;
},
configurable: true,
writable: true
});
}
const selectionStart = node?.selectionStart;
if (selectionStart == null) {
// $FlowFixMe[prop-missing]
Object.defineProperty(strictRef, 'selectionStart', {
get() {
return node._selectionStart ?? 0;
}
});
}
const selectionEnd = node?.selectionEnd;
if (selectionEnd == null) {
// $FlowFixMe[prop-missing]
Object.defineProperty(strictRef, 'selectionEnd', {
get() {
return node._selectionEnd ?? 0;
}
});
}
}

memoizedStrictRefs.set(node, strictRef);
Expand Down
1 change: 1 addition & 0 deletions packages/react-strict-dom/src/types/renderer.native.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ type ReactNativeProps = {
onPointerUp?: ViewProps['onPointerUp'],
onPress?: ?(event: PressEvent) => void,
onScroll?: $FlowFixMe,
onSelectionChange?: TextInputProps['onSelectionChange'],
onSubmitEditing?: TextInputProps['onSubmitEditing'],
onTouchCancel?: ViewProps['onTouchCancel'],
onTouchStart?: ViewProps['onTouchStart'],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ exports[`<compat.native> "as" equals "img": as=img 1`] = `
exports[`<compat.native> "as" equals "input": as=input 1`] = `
<TextInput
accessibilityLabel="label"
onSelectionChange={[Function]}
placeholderTextColor="green"
ref={[Function]}
secureTextEntry={true}
Expand All @@ -54,6 +55,7 @@ exports[`<compat.native> "as" equals "textarea": as=textarea 1`] = `
accessibilityLabel="label"
multiline={true}
numberOfLines={3}
onSelectionChange={[Function]}
ref={[Function]}
style={{}}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ exports[`css.* themes inherited themes 1`] = `
Expect color:green
</Text>,
<TextInput
onSelectionChange={[Function]}
placeholder="Expect placeholderTextColor:green"
placeholderTextColor="green"
ref={[Function]}
Expand Down Expand Up @@ -91,6 +92,7 @@ exports[`css.* themes inherited themes 1`] = `
Expect color:green (inherited)
</Text>
<TextInput
onSelectionChange={[Function]}
placeholder="Expect placeholderTextColor:green"
placeholderTextColor="green"
ref={[Function]}
Expand Down Expand Up @@ -125,6 +127,7 @@ exports[`css.* themes inherited themes 1`] = `
Expect color:blue (nested)
</Text>
<TextInput
onSelectionChange={[Function]}
placeholder="Expect placeholderTextColor:blue"
placeholderTextColor="blue"
ref={[Function]}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3126,6 +3126,7 @@ exports[`<html.*> "img" supports inline event handlers 1`] = `

exports[`<html.*> "input" default rendering 1`] = `
<TextInput
onSelectionChange={[Function]}
ref={[Function]}
style={
{
Expand All @@ -3139,6 +3140,7 @@ exports[`<html.*> "input" default rendering 1`] = `

exports[`<html.*> "input" ignores and warns about unsupported attributes 1`] = `
<TextInput
onSelectionChange={[Function]}
ref={[Function]}
style={
{
Expand All @@ -3158,6 +3160,7 @@ exports[`<html.*> "input" supports additional input attributes 1`] = `
focusable={false}
maxLength="10"
onChange={[Function]}
onSelectionChange={[Function]}
placeholder="Placeholder"
ref={[Function]}
style={
Expand Down Expand Up @@ -3206,6 +3209,7 @@ exports[`<html.*> "input" supports global attributes 1`] = `
importantForAccessibility="no-hide-descendants"
inputMode="numeric"
nativeID="some-id"
onSelectionChange={[Function]}
ref={[Function]}
role="article"
spellCheck={true}
Expand Down Expand Up @@ -3250,6 +3254,7 @@ exports[`<html.*> "input" supports inline event handlers 1`] = `
onPointerUp={[Function]}
onPress={[Function]}
onScroll={[Function]}
onSelectionChange={[Function]}
onSubmitEditing={[Function]}
onTouchCancel={[Function]}
onTouchEnd={[Function]}
Expand Down Expand Up @@ -5402,6 +5407,7 @@ exports[`<html.*> "sup" supports inline event handlers 1`] = `
exports[`<html.*> "textarea" default rendering 1`] = `
<TextInput
multiline={true}
onSelectionChange={[Function]}
ref={[Function]}
style={
{
Expand All @@ -5417,6 +5423,7 @@ exports[`<html.*> "textarea" default rendering 1`] = `
exports[`<html.*> "textarea" ignores and warns about unsupported attributes 1`] = `
<TextInput
multiline={true}
onSelectionChange={[Function]}
ref={[Function]}
style={
{
Expand All @@ -5439,6 +5446,7 @@ exports[`<html.*> "textarea" supports additional textarea attributes 1`] = `
multiline={true}
numberOfLines={3}
onChange={[Function]}
onSelectionChange={[Function]}
placeholder="Placeholder"
ref={[Function]}
style={
Expand Down Expand Up @@ -5488,6 +5496,7 @@ exports[`<html.*> "textarea" supports global attributes 1`] = `
importantForAccessibility="no-hide-descendants"
multiline={true}
nativeID="some-id"
onSelectionChange={[Function]}
ref={[Function]}
role="article"
spellCheck={true}
Expand Down Expand Up @@ -5534,6 +5543,7 @@ exports[`<html.*> "textarea" supports inline event handlers 1`] = `
onPointerUp={[Function]}
onPress={[Function]}
onScroll={[Function]}
onSelectionChange={[Function]}
onSubmitEditing={[Function]}
onTouchCancel={[Function]}
onTouchEnd={[Function]}
Expand Down
Loading