From d15c7a3277505510e0d05edff0c382a2af2d90dd Mon Sep 17 00:00:00 2001 From: Nicolas Gallagher Date: Fri, 22 Aug 2025 16:34:24 -0700 Subject: [PATCH 1/2] Use component syntax --- .eslintrc | 4 +- package-lock.json | 2 +- .../dom/modules/createStrictDOMComponent.js | 90 +++-- .../modules/createStrictDOMComponent.js | 308 +++++++++-------- .../modules/createStrictDOMImageComponent.js | 190 ++++++----- .../modules/createStrictDOMTextComponent.js | 192 ++++++----- .../createStrictDOMTextInputComponent.js | 317 +++++++++--------- 7 files changed, 543 insertions(+), 560 deletions(-) diff --git a/.eslintrc b/.eslintrc index a5841dd1..c5c65d75 100644 --- a/.eslintrc +++ b/.eslintrc @@ -2,8 +2,8 @@ "settings": { "react": { "pragma": "React", - "version": "18.0", - "flowVersion": "0.213.1" // Flow version + "version": "19.0", + "flowVersion": "0.248.1" // Flow version } }, // babel parser to support ES6/7 features diff --git a/package-lock.json b/package-lock.json index 8377a982..91012de5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31218,7 +31218,7 @@ } }, "packages/react-strict-dom": { - "version": "0.0.47", + "version": "0.0.48", "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.24.7", diff --git a/packages/react-strict-dom/src/dom/modules/createStrictDOMComponent.js b/packages/react-strict-dom/src/dom/modules/createStrictDOMComponent.js index ee7bff76..b69134e6 100644 --- a/packages/react-strict-dom/src/dom/modules/createStrictDOMComponent.js +++ b/packages/react-strict-dom/src/dom/modules/createStrictDOMComponent.js @@ -38,56 +38,50 @@ export function createStrictDOMComponent( 'debug::name': `html-${TagName}` as $FlowFixMe }; - const component: React.AbstractComponent = React.forwardRef( - function (props, forwardedRef) { - /** - * get host props - */ - const { for: htmlFor, style, ...restProps } = props; - const hostProps: { ...P, htmlFor?: string } = restProps; - validateStrictProps(hostProps); + component Component(ref?: React.RefSetter, ...props: P) { + /** + * get host props + */ + const { for: htmlFor, style, ...restProps } = props; + const hostProps: { ...P, htmlFor?: string } = restProps; + validateStrictProps(hostProps); - if (htmlFor != null) { - hostProps.htmlFor = htmlFor; - } - if (props.role != null) { - // "presentation" synonym has wider browser support - // $FlowFixMe - hostProps.role = props.role === 'none' ? 'presentation' : props.role; - } - if (TagName === 'button') { - hostProps.type = hostProps.type ? hostProps.type : 'button'; - } else if (TagName === 'input' || TagName === 'textarea') { - hostProps.dir = hostProps.dir ? hostProps.dir : 'auto'; - } + if (htmlFor != null) { + hostProps.htmlFor = htmlFor; + } + if (props.role != null) { + // "presentation" synonym has wider browser support + // $FlowFixMe + hostProps.role = props.role === 'none' ? 'presentation' : props.role; + } + if (TagName === 'button') { + hostProps.type = hostProps.type ? hostProps.type : 'button'; + } else if (TagName === 'input' || TagName === 'textarea') { + hostProps.dir = hostProps.dir ? hostProps.dir : 'auto'; + } - /** - * get host style props - */ - // Waiting on a diff so we can remove this indirection. - const hostStyleProps: ReactDOMStyleProps = stylex.props([ - debugStyle, - defaultStyle, - style - ]); + /** + * get host style props + */ + // Waiting on a diff so we can remove this indirection. + const hostStyleProps: ReactDOMStyleProps = stylex.props([ + debugStyle, + defaultStyle, + style + ]); - /** - * Construct tree - * - * Intentional flow error as we are asking for a more specific type - * than React itself. - */ - const element = ( - - ); - return element; - } - ); + /** + * Construct tree + * + * Intentional flow error as we are asking for a more specific type + * than React itself. + */ + const element = ( + + ); + return element; + } - component.displayName = `html.${TagName}`; - return component; + Component.displayName = `html.${TagName}`; + return Component; } diff --git a/packages/react-strict-dom/src/native/modules/createStrictDOMComponent.js b/packages/react-strict-dom/src/native/modules/createStrictDOMComponent.js index 23d63fb2..c8a51edc 100644 --- a/packages/react-strict-dom/src/native/modules/createStrictDOMComponent.js +++ b/packages/react-strict-dom/src/native/modules/createStrictDOMComponent.js @@ -35,184 +35,182 @@ export function createStrictDOMComponent( tagName: string, defaultProps?: P ): component(ref?: React.RefSetter, ...P) { - const component: React.AbstractComponent = React.forwardRef( - function (props, forwardedRef) { - let NativeComponent = - tagName === 'button' - ? ReactNative.Pressable - : ReactNative.ViewNativeComponent; - const elementRef = useStrictDOMElement(forwardedRef, { tagName }); - const hasTextAncestor = React.useContext(ReactNative.TextAncestorContext); - - /** - * Resolve global HTML and style props - */ - - const { customProperties, nativeProps, inheritableStyle } = - useNativeProps(defaultProps, props, { - provideInheritableStyle: - tagName !== 'br' || tagName !== 'hr' || tagName !== 'option', - withInheritedStyle: false, - withTextStyle: false - }); - - if ( - nativeProps.onPress != null && - NativeComponent === ReactNative.ViewNativeComponent - ) { - NativeComponent = ReactNative.Pressable; + const provideInheritableStyle = + tagName !== 'br' || tagName !== 'hr' || tagName !== 'option'; + + component Component(ref: React.RefSetter, ...props: P) { + let NativeComponent = + tagName === 'button' + ? ReactNative.Pressable + : ReactNative.ViewNativeComponent; + const elementRef = useStrictDOMElement(ref, { tagName }); + const hasTextAncestor = React.useContext(ReactNative.TextAncestorContext); + + /** + * Resolve global HTML and style props + */ + + const { customProperties, nativeProps, inheritableStyle } = useNativeProps( + defaultProps, + props, + { + provideInheritableStyle, + withInheritedStyle: false, + withTextStyle: false } + ); - // Tag-specific props + if ( + nativeProps.onPress != null && + NativeComponent === ReactNative.ViewNativeComponent + ) { + NativeComponent = ReactNative.Pressable; + } - if (tagName === 'button') { - nativeProps.role ??= 'button'; - } else if (tagName === 'header') { - nativeProps.role ??= 'header'; - } else if (tagName === 'li') { - nativeProps.role ??= 'listitem'; - } else if (tagName === 'ol' || tagName === 'ul') { - nativeProps.role ??= 'list'; - } + // Tag-specific props - // Component-specific props + if (tagName === 'button') { + nativeProps.role ??= 'button'; + } else if (tagName === 'header') { + nativeProps.role ??= 'header'; + } else if (tagName === 'li') { + nativeProps.role ??= 'listitem'; + } else if (tagName === 'ol' || tagName === 'ul') { + nativeProps.role ??= 'list'; + } - if (NativeComponent === ReactNative.Pressable) { - if (props.disabled === true) { - nativeProps.disabled = true; - nativeProps.focusable = false; - } + // Component-specific props + + if (NativeComponent === ReactNative.Pressable) { + if (props.disabled === true) { + nativeProps.disabled = true; + nativeProps.focusable = false; } + } - nativeProps.ref = elementRef; + nativeProps.ref = elementRef; - // Workaround: React Native doesn't support raw text children of View - // Sometimes we can auto-fix this - if (typeof nativeProps.children === 'string') { - nativeProps.children = ; - } + // Workaround: React Native doesn't support raw text children of View + // Sometimes we can auto-fix this + if (typeof nativeProps.children === 'string') { + nativeProps.children = ; + } - // Polyfill for default of "display:block" - // which implies "displayInside:flow" - let nextDisplayInsideValue = 'flow'; - const displayInsideValue = useDisplayInside(); - const displayValue = nativeProps.style.display; - - if (__DEV__) { - const nativeStyle = nativeProps.style; - if (displayInsideValue !== 'flex') { - // Error message if the element is not a flex child but tries to use flex - ['flex', 'flexBasis', 'flexGrow', 'flexShrink'].forEach( - (styleProp) => { - const value = nativeStyle[styleProp]; - if (value != null) { - errorMsg( - `"display:flex" is required on the parent for "${styleProp}" to have an effect.` - ); - } - } - ); - } + // Polyfill for default of "display:block" + // which implies "displayInside:flow" + let nextDisplayInsideValue = 'flow'; + const displayInsideValue = useDisplayInside(); + const displayValue = nativeProps.style.display; + + if (__DEV__) { + const nativeStyle = nativeProps.style; + if (displayInsideValue !== 'flex') { + // Error message if the element is not a flex child but tries to use flex + ['flex', 'flexBasis', 'flexGrow', 'flexShrink'].forEach((styleProp) => { + const value = nativeStyle[styleProp]; + if (value != null) { + errorMsg( + `"display:flex" is required on the parent for "${styleProp}" to have an effect.` + ); + } + }); } + } - if (displayValue === 'flex') { - nextDisplayInsideValue = 'flex'; - nativeProps.style.alignContent ??= 'stretch'; - nativeProps.style.alignItems ??= 'stretch'; - nativeProps.style.flexBasis ??= 'auto'; - nativeProps.style.flexDirection ??= 'row'; - nativeProps.style.flexShrink ??= 1; - nativeProps.style.flexWrap ??= 'nowrap'; - nativeProps.style.justifyContent ??= 'flex-start'; - } else if (displayValue === 'block' && displayInsideValue === 'flow') { - // Force the block emulation styles - nextDisplayInsideValue = 'flow'; - nativeProps.style.alignItems = 'stretch'; - nativeProps.style.display = 'flex'; - nativeProps.style.flexBasis = 'auto'; - nativeProps.style.flexDirection = 'column'; - nativeProps.style.flexShrink = 0; - nativeProps.style.flexWrap = 'nowrap'; - nativeProps.style.justifyContent = 'flex-start'; - } + if (displayValue === 'flex') { + nextDisplayInsideValue = 'flex'; + nativeProps.style.alignContent ??= 'stretch'; + nativeProps.style.alignItems ??= 'stretch'; + nativeProps.style.flexBasis ??= 'auto'; + nativeProps.style.flexDirection ??= 'row'; + nativeProps.style.flexShrink ??= 1; + nativeProps.style.flexWrap ??= 'nowrap'; + nativeProps.style.justifyContent ??= 'flex-start'; + } else if (displayValue === 'block' && displayInsideValue === 'flow') { + // Force the block emulation styles + nextDisplayInsideValue = 'flow'; + nativeProps.style.alignItems = 'stretch'; + nativeProps.style.display = 'flex'; + nativeProps.style.flexBasis = 'auto'; + nativeProps.style.flexDirection = 'column'; + nativeProps.style.flexShrink = 0; + nativeProps.style.flexWrap = 'nowrap'; + nativeProps.style.justifyContent = 'flex-start'; + } - if (displayInsideValue === 'flex') { - // flex child should not shrink by default - nativeProps.style.flexShrink ??= 1; - } + if (displayInsideValue === 'flex') { + // flex child should not shrink by default + nativeProps.style.flexShrink ??= 1; + } - // Use Animated components if necessary - if (nativeProps.animated === true) { - if (NativeComponent === ReactNative.ViewNativeComponent) { - NativeComponent = ReactNative.Animated.View; - } - if (NativeComponent === ReactNative.Pressable) { - NativeComponent = AnimatedPressable; - } + // Use Animated components if necessary + if (nativeProps.animated === true) { + if (NativeComponent === ReactNative.ViewNativeComponent) { + NativeComponent = ReactNative.Animated.View; } + if (NativeComponent === ReactNative.Pressable) { + NativeComponent = AnimatedPressable; + } + } - /** - * Construct tree - */ - - let element: React.Node = - typeof props.children === 'function' ? ( - props.children(nativeProps) - ) : ( - // $FlowFixMe - - ); + /** + * Construct tree + */ + + let element: React.Node = + typeof props.children === 'function' ? ( + props.children(nativeProps) + ) : ( + // $FlowFixMe + + ); + + // Enable W3C layout support + if (props['data-layoutconformance'] === 'strict') { + element = ( + + ); + } - // Enable W3C layout support - if (props['data-layoutconformance'] === 'strict') { + if ( + (nativeProps.children != null && + typeof nativeProps.children !== 'string') || + typeof props.children === 'function' + ) { + if (inheritableStyle != null) { element = ( - + ); } - - if ( - (nativeProps.children != null && - typeof nativeProps.children !== 'string') || - typeof props.children === 'function' - ) { - if (inheritableStyle != null) { - element = ( - - ); - } - if (nextDisplayInsideValue !== displayInsideValue) { - element = ( - - ); - } - if (customProperties != null) { - element = ( - - ); - } + if (nextDisplayInsideValue !== displayInsideValue) { + element = ( + + ); } - - if (hasTextAncestor) { - return ( - - {element} - + if (customProperties != null) { + element = ( + ); } + } - return element; + if (hasTextAncestor) { + return ( + + {element} + + ); } - ); - component.displayName = `html.${tagName}`; - return component; + return element; + } + + Component.displayName = `html.${tagName}`; + return Component; } diff --git a/packages/react-strict-dom/src/native/modules/createStrictDOMImageComponent.js b/packages/react-strict-dom/src/native/modules/createStrictDOMImageComponent.js index adb596ed..54508e62 100644 --- a/packages/react-strict-dom/src/native/modules/createStrictDOMImageComponent.js +++ b/packages/react-strict-dom/src/native/modules/createStrictDOMImageComponent.js @@ -20,106 +20,104 @@ export function createStrictDOMImageComponent( tagName: string, _defaultProps?: P ): component(ref?: React.RefSetter, ...P) { - const component: React.AbstractComponent = React.forwardRef( - function (props, forwardedRef) { - let NativeComponent = ReactNative.Image; - const elementRef = useStrictDOMElement(forwardedRef, { tagName }); - - const { - alt, - crossOrigin, - height, - onError, - onLoad, - referrerPolicy, - src, - srcSet, - width - } = props; - - /** - * Resolve global HTML and style props - */ - - const defaultProps = { - style: [ - _defaultProps?.style, - height != null && width != null && styles.aspectRatio(width, height) - ] + component Component(ref: React.RefSetter, ...props: P) { + let NativeComponent = ReactNative.Image; + const elementRef = useStrictDOMElement(ref, { tagName }); + + const { + alt, + crossOrigin, + height, + onError, + onLoad, + referrerPolicy, + src, + srcSet, + width + } = props; + + /** + * Resolve global HTML and style props + */ + + const defaultProps = { + style: [ + _defaultProps?.style, + height != null && width != null && styles.aspectRatio(width, height) + ] + }; + + const { nativeProps } = useNativeProps(defaultProps, props, { + provideInheritableStyle: false, + withInheritedStyle: false, + withTextStyle: false + }); + + // Tag-specific props + + if (alt != null) { + nativeProps.alt = alt; + } + if (crossOrigin != null) { + nativeProps.crossOrigin = crossOrigin; + } + if (height != null) { + nativeProps.height = height; + } + if (onError != null) { + nativeProps.onError = function () { + onError({ + type: 'error' + }); }; + } + if (onLoad != null) { + nativeProps.onLoad = function (e) { + const { source } = e.nativeEvent; + onLoad({ + target: { + naturalHeight: source?.height, + naturalWidth: source?.width + }, + type: 'load' + }); + }; + } + if (referrerPolicy != null) { + nativeProps.referrerPolicy = referrerPolicy; + } + if (src != null) { + nativeProps.src = src; + } + if (srcSet != null) { + nativeProps.srcSet = srcSet; + } + if (width != null) { + nativeProps.width = width; + } + + // Component-specific props - const { nativeProps } = useNativeProps(defaultProps, props, { - provideInheritableStyle: false, - withInheritedStyle: false, - withTextStyle: false - }); - - // Tag-specific props - - if (alt != null) { - nativeProps.alt = alt; - } - if (crossOrigin != null) { - nativeProps.crossOrigin = crossOrigin; - } - if (height != null) { - nativeProps.height = height; - } - if (onError != null) { - nativeProps.onError = function () { - onError({ - type: 'error' - }); - }; - } - if (onLoad != null) { - nativeProps.onLoad = function (e) { - const { source } = e.nativeEvent; - onLoad({ - target: { - naturalHeight: source?.height, - naturalWidth: source?.width - }, - type: 'load' - }); - }; - } - if (referrerPolicy != null) { - nativeProps.referrerPolicy = referrerPolicy; - } - if (src != null) { - nativeProps.src = src; - } - if (srcSet != null) { - nativeProps.srcSet = srcSet; - } - if (width != null) { - nativeProps.width = width; - } - - // Component-specific props - - nativeProps.ref = elementRef; - - // Use Animated components if necessary - if (nativeProps.animated === true) { - NativeComponent = ReactNative.Animated.Image; - } - - const element: React.Node = - typeof props.children === 'function' ? ( - props.children(nativeProps) - ) : ( - // $FlowFixMe - - ); - - return element; + nativeProps.ref = elementRef; + + // Use Animated components if necessary + if (nativeProps.animated === true) { + NativeComponent = ReactNative.Animated.Image; } - ); - component.displayName = `html.${tagName}`; - return component; + const element: React.Node = + typeof props.children === 'function' ? ( + props.children(nativeProps) + ) : ( + // $FlowFixMe + + ); + + return element; + } + + Component.displayName = `html.${tagName}`; + return Component; } const styles = css.create({ diff --git a/packages/react-strict-dom/src/native/modules/createStrictDOMTextComponent.js b/packages/react-strict-dom/src/native/modules/createStrictDOMTextComponent.js index 7835b79c..2bf7516a 100644 --- a/packages/react-strict-dom/src/native/modules/createStrictDOMTextComponent.js +++ b/packages/react-strict-dom/src/native/modules/createStrictDOMTextComponent.js @@ -32,114 +32,110 @@ export function createStrictDOMTextComponent( tagName: string, defaultProps?: P ): component(ref?: React.RefSetter, ...P) { - const component: React.AbstractComponent = React.forwardRef( - function (props, forwardedRef) { - let NativeComponent = ReactNative.Text; - const elementRef = useStrictDOMElement(forwardedRef, { tagName }); - - const { href, label } = props; - - /** - * Resolve global HTML and style props - */ - - const { customProperties, nativeProps, inheritableStyle } = - useNativeProps(defaultProps, props, { - provideInheritableStyle: - tagName !== 'br' || - tagName !== 'option' || - hasElementChildren(props.children), - withInheritedStyle: true, - withTextStyle: true - }); - - // Tag-specific props - - if (tagName === 'a') { - nativeProps.role ??= 'link'; - if (href != null) { - nativeProps.onPress = function (e) { - if (__DEV__) { - errorMsg( - ' "href" handling is not implemented in React Native.' - ); - } - }; - } - } else if (tagName === 'br') { - nativeProps.children = '\n'; - } else if ( - tagName === 'h1' || - tagName === 'h2' || - tagName === 'h3' || - tagName === 'h4' || - tagName === 'h5' || - tagName === 'h6' - ) { - nativeProps.role ??= 'heading'; - } else if (tagName === 'option') { - nativeProps.children = label; + component Component(ref?: React.RefSetter, ...props: P) { + let NativeComponent = ReactNative.Text; + const elementRef = useStrictDOMElement(ref, { tagName }); + + const { href, label } = props; + + /** + * Resolve global HTML and style props + */ + + const { customProperties, nativeProps, inheritableStyle } = useNativeProps( + defaultProps, + props, + { + provideInheritableStyle: + tagName !== 'br' || + tagName !== 'option' || + hasElementChildren(props.children), + withInheritedStyle: true, + withTextStyle: true } + ); + + // Tag-specific props + + if (tagName === 'a') { + nativeProps.role ??= 'link'; + if (href != null) { + nativeProps.onPress = function (e) { + if (__DEV__) { + errorMsg(' "href" handling is not implemented in React Native.'); + } + }; + } + } else if (tagName === 'br') { + nativeProps.children = '\n'; + } else if ( + tagName === 'h1' || + tagName === 'h2' || + tagName === 'h3' || + tagName === 'h4' || + tagName === 'h5' || + tagName === 'h6' + ) { + nativeProps.role ??= 'heading'; + } else if (tagName === 'option') { + nativeProps.children = label; + } - // Component-specific props + // Component-specific props - nativeProps.ref = elementRef; + nativeProps.ref = elementRef; - // Workaround: Android doesn't support ellipsis truncation if Text is selectable - // See #136 - const disableUserSelect = - ReactNative.Platform.OS === 'android' && - nativeProps.numberOfLines != null && - nativeProps.style.userSelect !== 'none'; + // Workaround: Android doesn't support ellipsis truncation if Text is selectable + // See #136 + const disableUserSelect = + ReactNative.Platform.OS === 'android' && + nativeProps.numberOfLines != null && + nativeProps.style.userSelect !== 'none'; - nativeProps.style = Object.assign( - nativeProps.style, - disableUserSelect ? { userSelect: 'none' } : null - ); + nativeProps.style = Object.assign( + nativeProps.style, + disableUserSelect ? { userSelect: 'none' } : null + ); - // Use Animated components if necessary - if (nativeProps.animated === true) { - NativeComponent = ReactNative.Animated.Text; - } + // Use Animated components if necessary + if (nativeProps.animated === true) { + NativeComponent = ReactNative.Animated.Text; + } - /** - * Construct tree - */ + /** + * Construct tree + */ - let element: React.Node = - typeof props.children === 'function' ? ( - props.children(nativeProps) - ) : ( - // $FlowFixMe - - ); + let element: React.Node = + typeof props.children === 'function' ? ( + props.children(nativeProps) + ) : ( + // $FlowFixMe + + ); - if ( - hasElementChildren(nativeProps.children) || - typeof props.children === 'function' - ) { - if (inheritableStyle != null) { - element = ( - - ); - } - if (customProperties != null) { - element = ( - - ); - } + if ( + hasElementChildren(nativeProps.children) || + typeof props.children === 'function' + ) { + if (inheritableStyle != null) { + element = ( + + ); + } + if (customProperties != null) { + element = ( + + ); } - - return element; } - ); - component.displayName = `html.${tagName}`; - return component; + return element; + } + + Component.displayName = `html.${tagName}`; + return Component; } diff --git a/packages/react-strict-dom/src/native/modules/createStrictDOMTextInputComponent.js b/packages/react-strict-dom/src/native/modules/createStrictDOMTextInputComponent.js index ed26de17..4b24793e 100644 --- a/packages/react-strict-dom/src/native/modules/createStrictDOMTextInputComponent.js +++ b/packages/react-strict-dom/src/native/modules/createStrictDOMTextInputComponent.js @@ -22,182 +22,179 @@ const AnimatedTextInput = ReactNative.Animated.createAnimatedComponent< typeof ReactNative.TextInput >(ReactNative.TextInput); -export function createStrictDOMTextInputComponent< - P: StrictReactDOMInputProps | StrictReactDOMTextAreaProps, - T ->( +type StrictInputProps = StrictReactDOMInputProps | StrictReactDOMTextAreaProps; + +export function createStrictDOMTextInputComponent( tagName: string, defaultProps?: P ): component(ref?: React.RefSetter, ...P) { - const component: React.AbstractComponent = React.forwardRef( - function (props, forwardedRef) { - let NativeComponent: - | typeof ReactNative.TextInput - | typeof AnimatedTextInput = ReactNative.TextInput; - const elementRef = useStrictDOMElement(forwardedRef, { tagName }); - - const { - autoComplete, - defaultValue, - disabled, - enterKeyHint, - inputMode, - maxLength, - onChange, - onInput, - onKeyDown, - placeholder, - readOnly, - rows, - spellCheck, - type, - value - } = props; - - /** - * Resolve global HTML and style props - */ - - const { nativeProps } = useNativeProps(defaultProps, props, { - provideInheritableStyle: false, - withInheritedStyle: false, - withTextStyle: true - }); - - // Tag-specific props - - if (tagName === 'input') { - let _inputMode = inputMode; - if (type === 'email') { - _inputMode = 'email'; - } - if (type === 'search') { - _inputMode = 'search'; - } - if (type === 'tel') { - _inputMode = 'tel'; - } - if (type === 'url') { - _inputMode = 'url'; - } - if (type === 'number') { - _inputMode = 'numeric'; - } - if (_inputMode != null) { - nativeProps.inputMode = _inputMode; - } - if (type === 'password') { - nativeProps.secureTextEntry = true; - } - if (type === 'checkbox' || type === 'date' || type === 'radio') { - if (__DEV__) { - errorMsg( - ` is not implemented in React Native.` - ); - } - } - } else if (tagName === 'textarea') { - nativeProps.multiline = true; - if (rows != null) { - nativeProps.numberOfLines = rows; - } + component Component(ref?: React.RefSetter, ...props: P) { + let NativeComponent: + | typeof ReactNative.TextInput + | typeof AnimatedTextInput = ReactNative.TextInput; + const elementRef = useStrictDOMElement(ref, { tagName }); + + const { + autoComplete, + defaultValue, + disabled, + enterKeyHint, + inputMode, + maxLength, + onChange, + onInput, + onKeyDown, + placeholder, + readOnly, + rows, + spellCheck, + type, + value + } = props; + + /** + * Resolve global HTML and style props + */ + + const { nativeProps } = useNativeProps(defaultProps, props, { + provideInheritableStyle: false, + withInheritedStyle: false, + withTextStyle: true + }); + + // Tag-specific props + + if (tagName === 'input') { + let _inputMode = inputMode; + if (type === 'email') { + _inputMode = 'email'; } - - // Component-specific props - - if (autoComplete != null) { - nativeProps.autoComplete = autoComplete; - } - if (defaultValue != null) { - nativeProps.defaultValue = defaultValue; - } - if (disabled === true) { - // polyfill disabled elements - nativeProps.disabled = true; - nativeProps.editable = false; - nativeProps.focusable = false; + if (type === 'search') { + _inputMode = 'search'; } - if (enterKeyHint != null) { - nativeProps.enterKeyHint = enterKeyHint; + if (type === 'tel') { + _inputMode = 'tel'; } - if (maxLength != null) { - nativeProps.maxLength = maxLength; + if (type === 'url') { + _inputMode = 'url'; } - if (onChange != null || onInput != null) { - nativeProps.onChange = function (e) { - const { text } = e.nativeEvent; - if (onInput != null) { - onInput({ - target: { - value: text - }, - type: 'input' - }); - } - if (onChange != null) { - onChange({ - target: { - value: text - }, - type: 'change' - }); - } - }; - } - if (onKeyDown != null) { - nativeProps.onKeyPress = function (e) { - const { key } = e.nativeEvent; - // Filter out bad iOS keypress data on submit - if ( - key === 'Backspace' || - (tagName === 'textarea' && key === 'Enter') || - key.length === 1 - ) { - onKeyDown({ - key, - type: 'keydown' - }); - } - }; - nativeProps.onSubmitEditing = function (e) { - onKeyDown({ - key: 'Enter', - type: 'keydown' - }); - }; + if (type === 'number') { + _inputMode = 'numeric'; } - if (placeholder != null) { - nativeProps.placeholder = placeholder; + if (_inputMode != null) { + nativeProps.inputMode = _inputMode; } - if (readOnly != null) { - nativeProps.editable = !readOnly; + if (type === 'password') { + nativeProps.secureTextEntry = true; } - if (spellCheck != null) { - nativeProps.spellCheck = spellCheck; + if (type === 'checkbox' || type === 'date' || type === 'radio') { + if (__DEV__) { + errorMsg( + ` is not implemented in React Native.` + ); + } } - if (value != null && typeof value === 'string') { - nativeProps.value = value; + } else if (tagName === 'textarea') { + nativeProps.multiline = true; + if (rows != null) { + nativeProps.numberOfLines = rows; } + } - nativeProps.ref = elementRef; + // Component-specific props - // Use Animated components if necessary - if (nativeProps.animated === true) { - NativeComponent = AnimatedTextInput; - } + if (autoComplete != null) { + nativeProps.autoComplete = autoComplete; + } + if (defaultValue != null) { + nativeProps.defaultValue = defaultValue; + } + if (disabled === true) { + // polyfill disabled elements + nativeProps.disabled = true; + nativeProps.editable = false; + nativeProps.focusable = false; + } + if (enterKeyHint != null) { + nativeProps.enterKeyHint = enterKeyHint; + } + if (maxLength != null) { + nativeProps.maxLength = maxLength; + } + if (onChange != null || onInput != null) { + nativeProps.onChange = function (e) { + const { text } = e.nativeEvent; + if (onInput != null) { + onInput({ + target: { + value: text + }, + type: 'input' + }); + } + if (onChange != null) { + onChange({ + target: { + value: text + }, + type: 'change' + }); + } + }; + } + if (onKeyDown != null) { + nativeProps.onKeyPress = function (e) { + const { key } = e.nativeEvent; + // Filter out bad iOS keypress data on submit + if ( + key === 'Backspace' || + (tagName === 'textarea' && key === 'Enter') || + key.length === 1 + ) { + onKeyDown({ + key, + type: 'keydown' + }); + } + }; + nativeProps.onSubmitEditing = function (e) { + onKeyDown({ + key: 'Enter', + type: 'keydown' + }); + }; + } + if (placeholder != null) { + nativeProps.placeholder = placeholder; + } + if (readOnly != null) { + nativeProps.editable = !readOnly; + } + if (spellCheck != null) { + nativeProps.spellCheck = spellCheck; + } + if (value != null && typeof value === 'string') { + nativeProps.value = value; + } - const element = - typeof props.children === 'function' ? ( - props.children(nativeProps) - ) : ( - // $FlowFixMe - - ); + nativeProps.ref = elementRef; - return element; + // Use Animated components if necessary + if (nativeProps.animated === true) { + NativeComponent = AnimatedTextInput; } - ); - component.displayName = `html.${tagName}`; - return component; + const element = + typeof props.children === 'function' ? ( + props.children(nativeProps) + ) : ( + // $FlowFixMe + + ); + + return element; + } + + Component.displayName = `html.${tagName}`; + return Component; } From b0ef837cb55b814757ad74397315330024445d45 Mon Sep 17 00:00:00 2001 From: Nicolas Gallagher Date: Fri, 22 Aug 2025 21:02:11 -0700 Subject: [PATCH 2/2] Install react-hooks@6.1.0-canary and hermes-eslint --- package-lock.json | 73 +++++++++++++------ package.json | 3 +- .../dom/modules/createStrictDOMComponent.js | 1 + .../modules/createStrictDOMComponent.js | 1 + .../modules/createStrictDOMImageComponent.js | 1 + .../modules/createStrictDOMTextComponent.js | 1 + .../createStrictDOMTextInputComponent.js | 1 + 7 files changed, 58 insertions(+), 23 deletions(-) diff --git a/package-lock.json b/package-lock.json index 91012de5..6551d9ec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,9 +30,10 @@ "eslint-plugin-ft-flow": "^3.0.7", "eslint-plugin-promise": "^6.0.0", "eslint-plugin-react": "^7.33.1", - "eslint-plugin-react-hooks": "^4.5.0", + "eslint-plugin-react-hooks": "6.1.0-canary-12bc60f5-20250613", "flow-api-translator": "^0.25.0", "flow-bin": "^0.248.1", + "hermes-eslint": "^0.32.0", "husky": "^8.0.0", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", @@ -13156,16 +13157,24 @@ } }, "node_modules/eslint-plugin-react-hooks": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", - "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", + "version": "6.1.0-canary-12bc60f5-20250613", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-6.1.0-canary-12bc60f5-20250613.tgz", + "integrity": "sha512-K+594rswc9TF1sxVO/Mp5QxxgD6tDsFT/FiwJ+O0/z9+Id6IKUyLn5S7TP24sIij6caUE6aRSEaOqkeIqmzK9A==", "dev": true, "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "@babel/plugin-transform-private-methods": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.22.4", + "zod-validation-error": "^3.0.3" + }, "engines": { - "node": ">=10" + "node": ">=18" }, "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "node_modules/eslint-plugin-react/node_modules/doctrine": { @@ -15778,35 +15787,32 @@ } }, "node_modules/hermes-eslint": { - "version": "0.29.0", - "resolved": "https://registry.npmjs.org/hermes-eslint/-/hermes-eslint-0.29.0.tgz", - "integrity": "sha512-bEV507Pw5hyeZIKJHn+1RmaAPA3tmHRlkm1q3GBcCIO28CTmVURYeZnrW8M7j7GnaFodlzP2mqvJBkk/XHFacA==", + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/hermes-eslint/-/hermes-eslint-0.32.0.tgz", + "integrity": "sha512-f/gnFD3Nl7QNrclG6otkHnHsUbwYrJGO76AMtoDeIYs2+i7fFgqJgSg7DKwejTtAKBoXQg51hAQuo9cgcp1R1w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esrecurse": "^4.3.0", - "hermes-estree": "0.29.0", - "hermes-parser": "0.29.0" + "hermes-estree": "0.32.0", + "hermes-parser": "0.32.0" } }, "node_modules/hermes-eslint/node_modules/hermes-estree": { - "version": "0.29.0", - "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.29.0.tgz", - "integrity": "sha512-Dz8xXk5v9HfLCJBfftELIKq5A2hITlvqRf5h1wUVnxxL9cb4B/XKm0FdxxH3OUnYSkjF/8V1t0rN7mI8WraPMg==", + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.32.0.tgz", + "integrity": "sha512-KWn3BqnlDOl97Xe1Yviur6NbgIZ+IP+UVSpshlZWkq+EtoHg6/cwiDj/osP9PCEgFE15KBm1O55JRwbMEm5ejQ==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/hermes-eslint/node_modules/hermes-parser": { - "version": "0.29.0", - "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.29.0.tgz", - "integrity": "sha512-7o4b0LJlsWDMrGx0T14qDFFcTtkQDqH4HVuGmQ9XYBIPc9lGCDGkXUjDX4Fi+gQYzhPFqzj2TotWXKQo4SIzyQ==", + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.32.0.tgz", + "integrity": "sha512-g4nBOWFpuiTqjR3LZdRxKUkij9iyveWeuks7INEsMX741f3r9xxrOe8TeQfUxtda0eXmiIFiMQzoeSQEno33Hw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "hermes-estree": "0.29.0" + "hermes-estree": "0.32.0" } }, "node_modules/hermes-estree": { @@ -31190,6 +31196,29 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-3.5.3.tgz", + "integrity": "sha512-OT5Y8lbUadqVZCsnyFaTQ4/O2mys4tj7PqhdbBCp7McPwvIEKfPtdA6QfPeFQK2/Rz5LgwmAXRJTugBNBi0btw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + }, "node_modules/zwitch": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", diff --git a/package.json b/package.json index c80c605d..fe16653f 100644 --- a/package.json +++ b/package.json @@ -32,9 +32,10 @@ "eslint-plugin-ft-flow": "^3.0.7", "eslint-plugin-promise": "^6.0.0", "eslint-plugin-react": "^7.33.1", - "eslint-plugin-react-hooks": "^4.5.0", + "eslint-plugin-react-hooks": "6.1.0-canary-12bc60f5-20250613", "flow-api-translator": "^0.25.0", "flow-bin": "^0.248.1", + "hermes-eslint": "^0.32.0", "husky": "^8.0.0", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", diff --git a/packages/react-strict-dom/src/dom/modules/createStrictDOMComponent.js b/packages/react-strict-dom/src/dom/modules/createStrictDOMComponent.js index b69134e6..405c98ae 100644 --- a/packages/react-strict-dom/src/dom/modules/createStrictDOMComponent.js +++ b/packages/react-strict-dom/src/dom/modules/createStrictDOMComponent.js @@ -82,6 +82,7 @@ export function createStrictDOMComponent( return element; } + // eslint-disable-next-line no-unreachable Component.displayName = `html.${TagName}`; return Component; } diff --git a/packages/react-strict-dom/src/native/modules/createStrictDOMComponent.js b/packages/react-strict-dom/src/native/modules/createStrictDOMComponent.js index c8a51edc..55a510bc 100644 --- a/packages/react-strict-dom/src/native/modules/createStrictDOMComponent.js +++ b/packages/react-strict-dom/src/native/modules/createStrictDOMComponent.js @@ -211,6 +211,7 @@ export function createStrictDOMComponent( return element; } + // eslint-disable-next-line no-unreachable Component.displayName = `html.${tagName}`; return Component; } diff --git a/packages/react-strict-dom/src/native/modules/createStrictDOMImageComponent.js b/packages/react-strict-dom/src/native/modules/createStrictDOMImageComponent.js index 54508e62..52a4be12 100644 --- a/packages/react-strict-dom/src/native/modules/createStrictDOMImageComponent.js +++ b/packages/react-strict-dom/src/native/modules/createStrictDOMImageComponent.js @@ -116,6 +116,7 @@ export function createStrictDOMImageComponent( return element; } + // eslint-disable-next-line no-unreachable Component.displayName = `html.${tagName}`; return Component; } diff --git a/packages/react-strict-dom/src/native/modules/createStrictDOMTextComponent.js b/packages/react-strict-dom/src/native/modules/createStrictDOMTextComponent.js index 2bf7516a..b0660e43 100644 --- a/packages/react-strict-dom/src/native/modules/createStrictDOMTextComponent.js +++ b/packages/react-strict-dom/src/native/modules/createStrictDOMTextComponent.js @@ -136,6 +136,7 @@ export function createStrictDOMTextComponent( return element; } + // eslint-disable-next-line no-unreachable Component.displayName = `html.${tagName}`; return Component; } diff --git a/packages/react-strict-dom/src/native/modules/createStrictDOMTextInputComponent.js b/packages/react-strict-dom/src/native/modules/createStrictDOMTextInputComponent.js index 4b24793e..6a7217b9 100644 --- a/packages/react-strict-dom/src/native/modules/createStrictDOMTextInputComponent.js +++ b/packages/react-strict-dom/src/native/modules/createStrictDOMTextInputComponent.js @@ -195,6 +195,7 @@ export function createStrictDOMTextInputComponent( return element; } + // eslint-disable-next-line no-unreachable Component.displayName = `html.${tagName}`; return Component; }