diff --git a/.buckconfig b/.buckconfig index 0740647479601e..07ec813646f170 100644 --- a/.buckconfig +++ b/.buckconfig @@ -1,6 +1,6 @@ [android] - target = android-29 + target = android-30 [download] max_number_of_retries = 3 @@ -8,7 +8,6 @@ [maven_repositories] central = https://repo1.maven.org/maven2 google = https://maven.google.com/ - jcenter = https://jcenter.bintray.com/ [alias] rntester = //packages/rn-tester/android/app:app diff --git a/.circleci/Dockerfiles/Dockerfile.android b/.circleci/Dockerfiles/Dockerfile.android index 23513414924ed2..96a361b9fa5ebb 100644 --- a/.circleci/Dockerfiles/Dockerfile.android +++ b/.circleci/Dockerfiles/Dockerfile.android @@ -14,7 +14,7 @@ # and build a Android application that can be used to run the # tests specified in the scripts/ directory. # -FROM reactnativecommunity/react-native-android:2.1 +FROM reactnativecommunity/react-native-android:4.0 LABEL Description="React Native Android Test Image" LABEL maintainer="Héctor Ramos " @@ -26,6 +26,7 @@ ENV JAVA_TOOL_OPTIONS="-Dfile.encoding=UTF8" ADD .buckconfig /app/.buckconfig ADD .buckjavaargs /app/.buckjavaargs +ADD BUCK /app/BUCK ADD Libraries /app/Libraries ADD ReactAndroid /app/ReactAndroid ADD ReactCommon /app/ReactCommon diff --git a/.circleci/config.yml b/.circleci/config.yml index 8d15b406196098..baa53e7220cf91 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -15,10 +15,10 @@ defaults: &defaults environment: - GIT_COMMIT_DESC: git log --format=oneline -n 1 $CIRCLE_SHA1 # The public github tokens are publicly visible by design - - PUBLIC_PULLBOT_GITHUB_TOKEN_A: "a6edf8e8d40ce4e8b11a" - - PUBLIC_PULLBOT_GITHUB_TOKEN_B: "150e1341f4dd9c944d2a" - - PUBLIC_ANALYSISBOT_GITHUB_TOKEN_A: &github_token_a "78a72af35445ca3f8180" - - PUBLIC_ANALYSISBOT_GITHUB_TOKEN_B: &github_token_b "b1a98e0bbd56ff1ccba1" + - PUBLIC_PULLBOT_GITHUB_TOKEN_A: &github_pullbot_token_a "a6edf8e8d40ce4e8b11a" + - PUBLIC_PULLBOT_GITHUB_TOKEN_B: &github_pullbot_token_b "150e1341f4dd9c944d2a" + - PUBLIC_ANALYSISBOT_GITHUB_TOKEN_A: &github_analysisbot_token_a "312d354b5c36f082cfe9" + - PUBLIC_ANALYSISBOT_GITHUB_TOKEN_B: &github_analysisbot_token_b "07973d757026bdd9f196" # ------------------------- # EXECUTORS @@ -27,6 +27,7 @@ executors: nodelts: <<: *defaults docker: + # Note: Version set separately for Windows builds, see below. - image: circleci/node:14 nodeprevlts: <<: *defaults @@ -35,7 +36,7 @@ executors: reactnativeandroid: <<: *defaults docker: - - image: reactnativecommunity/react-native-android:2.1 + - image: reactnativecommunity/react-native-android:4.0 resource_class: "large" environment: - TERM: "dumb" @@ -44,12 +45,14 @@ executors: - GRADLE_OPTS: '-Dorg.gradle.daemon=false -Dorg.gradle.jvmargs="-XX:+HeapDumpOnOutOfMemoryError"' - BUILD_THREADS: 2 # Repeated here, as the environment key in this executor will overwrite the one in defaults - - PUBLIC_ANALYSISBOT_GITHUB_TOKEN_A: *github_token_a - - PUBLIC_ANALYSISBOT_GITHUB_TOKEN_B: *github_token_b + - PUBLIC_ANALYSISBOT_GITHUB_TOKEN_A: *github_analysisbot_token_a + - PUBLIC_ANALYSISBOT_GITHUB_TOKEN_B: *github_analysisbot_token_b + - PUBLIC_PULLBOT_GITHUB_TOKEN_A: *github_pullbot_token_a + - PUBLIC_PULLBOT_GITHUB_TOKEN_B: *github_pullbot_token_b reactnativeios: <<: *defaults macos: - xcode: &_XCODE_VERSION "12.1.0" + xcode: &_XCODE_VERSION "12.5.0" # ------------------------- # COMMANDS @@ -75,7 +78,6 @@ commands: - restore_cache: keys: - v4-yarn-cache-{{ arch }}-{{ checksum "yarn.lock" }} - - v4-yarn-cache-{{ arch }} - run: name: "Yarn: Install Dependencies" command: | @@ -94,7 +96,6 @@ commands: - restore_cache: keys: - v3-buck-v2019.01.10.01-{{ checksum "scripts/circleci/buck_fetch.sh" }}} - - v3-buck-v2019.01.10.01- - run: name: Install BUCK command: | @@ -151,20 +152,20 @@ commands: command: cp packages/rn-tester/Podfile.lock packages/rn-tester/Podfile.lock.bak - restore_cache: keys: - - v3-pods-{{ .Environment.CIRCLE_JOB }}-{{ checksum "packages/rn-tester/Podfile.lock.bak" }} - - v3-pods-{{ .Environment.CIRCLE_JOB }}- + # The committed lockfile is generated using USE_FRAMEWORKS=0 and USE_HERMES=0 so it could load an outdated cache if a change + # only affects the frameworks or hermes config. To help prevent this also cache based on the content of Podfile. + - v3-pods-{{ .Environment.CIRCLE_JOB }}-{{ checksum "packages/rn-tester/Podfile.lock.bak" }}-{{ checksum "packages/rn-tester/Podfile" }} - steps: << parameters.steps >> - save_cache: paths: - packages/rn-tester/Pods - key: v3-pods-{{ .Environment.CIRCLE_JOB }}-{{ checksum "packages/rn-tester/Podfile.lock.bak" }} + key: v3-pods-{{ .Environment.CIRCLE_JOB }}-{{ checksum "packages/rn-tester/Podfile.lock.bak" }}-{{ checksum "packages/rn-tester/Podfile" }} download_gradle_dependencies: steps: - restore_cache: keys: - v1-gradle-{{ checksum "ReactAndroid/build.gradle" }}-{{ checksum "scripts/circleci/gradle_download_deps.sh" }} - - v1-gradle- - run: name: Download Dependencies Using Gradle command: ./scripts/circleci/gradle_download_deps.sh @@ -236,17 +237,20 @@ jobs: # Issues will be posted to the PR itself via GitHub bots. # This workflow should only fail if the bots fail to run. analyze_pr: - executor: nodelts + executor: reactnativeandroid steps: - restore_cache_checkout: - checkout_type: node + checkout_type: android - run_yarn - install_github_bot_deps + # Note: The yarn gpg key needs to be refreshed to work around https://github.com/yarnpkg/yarn/issues/7866 - run: name: Install additional GitHub bot dependencies - command: sudo apt update && sudo apt install -y shellcheck jq + command: | + curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - + apt update && apt install -y shellcheck jq - run: name: Run linters against modified files (analysis-bot) @@ -264,10 +268,10 @@ jobs: # JOBS: Analyze Code # ------------------------- analyze_code: - executor: nodelts + executor: reactnativeandroid steps: - restore_cache_checkout: - checkout_type: node + checkout_type: android - setup_artifacts - run_yarn @@ -276,6 +280,11 @@ jobs: command: scripts/circleci/exec_swallow_error.sh yarn lint --format junit -o ./reports/junit/eslint/results.xml when: always + - run: + name: Lint Java + command: scripts/circleci/exec_swallow_error.sh yarn lint-java --check + when: always + - run: name: Check for errors in code using Flow (iOS) command: yarn flow-check-ios @@ -423,7 +432,7 @@ jobs: steps: - run: name: Generate RNTesterPods Workspace - command: cd packages/rn-tester && bundle exec pod install --verbose + command: cd packages/rn-tester && USE_FABRIC=1 bundle exec pod install --verbose # ------------------------- # Runs iOS unit tests @@ -495,12 +504,6 @@ jobs: command: source scripts/android-setup.sh && launchAVD background: true - # Keep configuring Android dependencies while AVD boots up - - - run: - name: Install rsync, zip - command: apt-get update -y && apt-get install rsync zip -y - # Install Buck - install_buck_tooling @@ -606,14 +609,21 @@ jobs: default: false environment: - ANDROID_HOME: "C:\\Android\\android-sdk" - - ANDROID_NDK: "C:\\Android\\android-sdk\\ndk\\19.2.5345600" - - ANDROID_BUILD_VERSION: 28 - - ANDROID_TOOLS_VERSION: 29.0.3 + - ANDROID_NDK: "C:\\Android\\android-sdk\\ndk\\20.1.5948944" + - ANDROID_BUILD_VERSION: 30 + - ANDROID_TOOLS_VERSION: 30.0.2 - GRADLE_OPTS: -Dorg.gradle.daemon=false - - NDK_VERSION: 19.2.5345600 + - NDK_VERSION: 21.4.7075529 steps: - checkout + - run: + name: Install Node + # Note: Version set separately for non-Windows builds, see above. + command: | + nvm install 14.17.0 + nvm use 14.17.0 + # Setup Dependencies - run: name: Install Yarn @@ -626,7 +636,6 @@ jobs: - restore_cache: keys: - v1-win-yarn-cache-{{ arch }}-{{ checksum "yarn.lock" }} - - v1-win-yarn-cache-{{ arch }}- - run: name: "Yarn: Install Dependencies" command: yarn install --frozen-lockfile --non-interactive @@ -635,9 +644,11 @@ jobs: paths: - C:\Users\circleci\AppData\Local\Yarn + # Try to install the SDK up to 3 times, since network flakiness can cause install failures + # Using a timeout of 9 mins, as circle ci will timeout if there is no output for 10 mins - run: name: Install Android SDK Tools - command: choco install android-sdk + command: choco install android-sdk --timeout 540; if (!$?) { choco install android-sdk --timeout 540 --force --forcedependencies}; if (!$?) { choco install android-sdk --force --forcedependencies} - run: name: Setup Android SDKs @@ -778,30 +789,34 @@ workflows: run_unit_tests: true requires: - setup_ios - - test_ios: - name: test_ios_unit_frameworks_jsc - use_frameworks: true - run_unit_tests: true - requires: - - setup_ios + # DISABLED: USE_FRAMEWORKS=1 not supported by Flipper + # - test_ios: + # name: test_ios_unit_frameworks_jsc + # use_frameworks: true + # run_unit_tests: true + # requires: + # - setup_ios - test_ios: name: test_ios_unit_hermes use_hermes: true run_unit_tests: true requires: - setup_ios - - test_ios: - name: test_ios_unit_frameworks_hermes - use_hermes: true - use_frameworks: true - run_unit_tests: true - requires: - - setup_ios + # DISABLED: USE_FRAMEWORKS=1 not supported by Flipper + # - test_ios: + # name: test_ios_unit_frameworks_hermes + # use_hermes: true + # use_frameworks: true + # run_unit_tests: true + # requires: + # - setup_ios + # DISABLED: Detox tests need to be fixed # - test_ios: # name: test_ios_detox # run_detox_tests: true # requires: # - setup_ios + # DISABLED: USE_FRAMEWORKS=1 not supported by Flipper # - test_ios: # name: test_ios_detox_frameworks # use_frameworks: true @@ -856,11 +871,18 @@ workflows: analysis: jobs: - - setup + - setup: + name: setup_js + + - setup: + name: setup_android + checkout_type: android + executor: reactnativeandroid + # Run lints on every commit other than those to the gh-pages branch - analyze_code: requires: - - setup + - setup_android filters: branches: ignore: gh-pages @@ -868,7 +890,7 @@ workflows: # Run code checks on PRs from forks - analyze_pr: requires: - - setup + - setup_android filters: branches: only: /^pull\/.*$/ @@ -876,7 +898,7 @@ workflows: # Gather coverage - js_coverage: requires: - - setup + - setup_js # [TODO(macOS GH#774): disable this test, it fails in the fork due to not being set up for coveralls filters: branches: @@ -891,7 +913,7 @@ workflows: # [TODO(macOS GH#774): disable this release. We never want to release anything from this fork via CCI. ignore: /.*/ # only: - # - master + # - main # ]TODO(macOS GH#774) jobs: - nightly_job diff --git a/.editorconfig b/.editorconfig index 45dc2a9a3fecd4..355a800148a2d9 100644 --- a/.editorconfig +++ b/.editorconfig @@ -13,5 +13,12 @@ indent_size = 2 [*.gradle] indent_size = 4 +[*.kts] +indent_size = 4 + [BUCK] indent_size = 4 + +# Windows files +[*.bat] +end_of_line = crlf diff --git a/.eslintrc b/.eslintrc index 2c5a51053d8221..f990717831deb2 100644 --- a/.eslintrc +++ b/.eslintrc @@ -21,6 +21,14 @@ "@react-native/codegen/react-native-modules": 2 } }, + { + "files": [ + "flow-typed/**/*.js", + ], + "rules": { + quotes: 0 + } + }, { "files": [ "**/__fixtures__/**/*.js", diff --git a/.flowconfig b/.flowconfig index e45052fc3424cd..f763d569d9486c 100644 --- a/.flowconfig +++ b/.flowconfig @@ -35,6 +35,9 @@ flow/ emoji=true exact_by_default=true +indexed_access=false + +format.bracket_spacing=false module.file_ext=.js module.file_ext=.json @@ -52,15 +55,13 @@ suppress_type=$FlowFixMeProps suppress_type=$FlowFixMeState suppress_type=$FlowFixMeEmpty -experimental.abstract_locations=true - [lints] sketchy-null-number=warn sketchy-null-mixed=warn sketchy-number=warn untyped-type-import=warn nonstrict-import=warn -deprecated-type=warn +deprecated-type=error unsafe-getters-setters=warn unnecessary-invariant=warn signature-verification-failure=warn @@ -75,4 +76,4 @@ untyped-import untyped-type-import [version] -^0.137.0 +^0.158.0 diff --git a/.flowconfig.android b/.flowconfig.android index 1fe01b8ea01f8b..ff3d0c92033437 100644 --- a/.flowconfig.android +++ b/.flowconfig.android @@ -35,6 +35,9 @@ flow/ emoji=true exact_by_default=true +indexed_access=false + +format.bracket_spacing=false module.file_ext=.js module.file_ext=.json @@ -55,15 +58,13 @@ suppress_type=$FlowFixMeProps suppress_type=$FlowFixMeState suppress_type=$FlowFixMeEmpty -experimental.abstract_locations=true - [lints] sketchy-null-number=warn sketchy-null-mixed=warn sketchy-number=warn untyped-type-import=warn nonstrict-import=warn -deprecated-type=warn +deprecated-type=error unsafe-getters-setters=warn unnecessary-invariant=warn signature-verification-failure=warn @@ -78,4 +79,4 @@ untyped-import untyped-type-import [version] -^0.137.0 +^0.158.0 diff --git a/.flowconfig.macos b/.flowconfig.macos index 46e8755cec24ac..08ffca1b0204e2 100644 --- a/.flowconfig.macos +++ b/.flowconfig.macos @@ -34,9 +34,6 @@ flow/ [options] emoji=true -esproposal.optional_chaining=enable -esproposal.nullish_coalescing=enable - exact_by_default=true module.file_ext=.js @@ -55,8 +52,6 @@ suppress_type=$FlowFixMeProps suppress_type=$FlowFixMeState suppress_type=$FlowFixMeEmpty -well_formed_exports=true -types_first=true experimental.abstract_locations=true [lints] @@ -81,4 +76,4 @@ untyped-import untyped-type-import [version] -^0.137.0 +^0.158.0 diff --git a/.gitattributes b/.gitattributes index eaf618cafb2032..7c973903433b65 100644 --- a/.gitattributes +++ b/.gitattributes @@ -3,4 +3,4 @@ *.sh text eol=lf # Windows files should use crlf line endings # https://help.github.com/articles/dealing-with-line-endings/ -*.bat text eol=crlf \ No newline at end of file +*.bat text eol=crlf diff --git a/.github/SUPPORT.md b/.github/SUPPORT.md index 4e313c997e3094..e597d1e3334e5a 100644 --- a/.github/SUPPORT.md +++ b/.github/SUPPORT.md @@ -27,7 +27,7 @@ If you'd like to discuss topics related to the future of React Native, please ch If you want to participate in casual discussions about the use of React Native, consider participating in one of the following forums: -- [Reactiflux Discord Server](https://www.reactiflux) +- [Reactiflux Discord Server](https://www.reactiflux.com) - [Spectrum Chat](https://spectrum.chat/react-native) - [React Native Community Facebook Group](https://www.facebook.com/groups/react.native.community) diff --git a/.github/label-actions.yml b/.github/respond-to-issue-based-on-label.yml similarity index 96% rename from .github/label-actions.yml rename to .github/respond-to-issue-based-on-label.yml index 8cef0647e0068c..65131ff0840db0 100644 --- a/.github/label-actions.yml +++ b/.github/respond-to-issue-based-on-label.yml @@ -1,4 +1,4 @@ -# Configuration for Label Actions - https://github.com/marketplace/actions/label-actions +# Configuration for Respond To Issue Based on Label https://github.com/marketplace/actions/respond-to-issue-based-on-label "Type: Invalid": close: true diff --git a/.github/workflows/process-label-actions.yml b/.github/workflows/on-issue-labeled.yml similarity index 54% rename from .github/workflows/process-label-actions.yml rename to .github/workflows/on-issue-labeled.yml index 2c63100f6e8392..e68682b2ecb71b 100644 --- a/.github/workflows/process-label-actions.yml +++ b/.github/workflows/on-issue-labeled.yml @@ -1,16 +1,16 @@ -name: Label Actions +name: On Issue Labeled # This workflow is triggered when a label is added to an issue. on: issues: types: labeled jobs: - processLabelAction: - name: Process Label Action + respondToIssueBasedOnLabel: + name: Respond to Issue Based on Label runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - name: Process Label Action - uses: hramos/label-actions@v1 + - name: Respond to Issue Based on Label + uses: hramos/respond-to-issue-based-on-label@v2 with: repo-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index c9ad5eacf11c52..eebf7dd21c678b 100644 --- a/.gitignore +++ b/.gitignore @@ -27,7 +27,7 @@ project.xcworkspace/* # Gradle /build/ /packages/react-native-codegen/android/build/ -/packages/react-native-codegen/android/gradlePlugin-build/gradlePlugin/build +/packages/react-native-gradle-plugin/build/ /packages/rn-tester/android/app/.cxx/ /packages/rn-tester/android/app/build/ /packages/rn-tester/android/app/gradle/ @@ -94,9 +94,9 @@ package-lock.json # Libs that shouldn't have Xcode project /Libraries/FBLazyVector/**/*.xcodeproj -/Libraries/FBReactNativeSpec/**/*.xcodeproj /Libraries/RCTRequired/**/*.xcodeproj /React/CoreModules/**/*.xcodeproj +/React/FBReactNativeSpec/**/*.xcodeproj /packages/react-native-codegen/**/*.xcodeproj # CocoaPods @@ -109,10 +109,10 @@ package-lock.json !/packages/rn-tester/Pods/__offline_mirrors__ # react-native-codegen -/Libraries/FBReactNativeSpec/FBReactNativeSpec +/React/FBReactNativeSpec/FBReactNativeSpec /packages/react-native-codegen/lib -/ReactCommon/fabric/components/rncore/ -/schema-rncore.json +/ReactCommon/react/renderer/components/rncore/ +/packages/rn-tester/NativeModuleExample/ScreenshotManagerSpec* # Visual studio .vscode diff --git a/.nvmrc b/.nvmrc deleted file mode 100644 index dba04c1e1786b2..00000000000000 --- a/.nvmrc +++ /dev/null @@ -1 +0,0 @@ -8.11.3 diff --git a/BUCK b/BUCK new file mode 100644 index 00000000000000..b315295c974f53 --- /dev/null +++ b/BUCK @@ -0,0 +1,1386 @@ +load("//tools/build_defs:fb_native_wrapper.bzl", "fb_native") +load("//tools/build_defs/apple:config_utils_defs.bzl", "STATIC_LIBRARY_APPLETVOS_CONFIG", "fbobjc_configs") +load("//tools/build_defs/apple:fb_apple_test.bzl", "fb_apple_test") +load("//tools/build_defs/apple:flag_defs.bzl", "get_base_appletvos_flags", "get_objc_arc_preprocessor_flags", "get_preprocessor_flags_for_build_mode", "get_static_library_ios_flags") +load("//tools/build_defs/apple/plugins:plugin_defs.bzl", "plugin") +load("//tools/build_defs/oss:metro_defs.bzl", "rn_library") +load( + "//tools/build_defs/oss:rn_codegen_defs.bzl", + "rn_codegen", + "rn_codegen_components", +) +load( + "//tools/build_defs/oss:rn_defs.bzl", + "APPLETVOS", + "HERMES_BYTECODE_VERSION", + "IOS", + "RCT_IMAGE_DATA_DECODER_SOCKET", + "RCT_IMAGE_URL_LOADER_SOCKET", + "RCT_URL_REQUEST_HANDLER_SOCKET", + "YOGA_CXX_TARGET", + "react_fabric_component_plugin_provider", + "react_module_plugin_providers", + "react_native_root_target", + "react_native_xplat_dep", + "react_native_xplat_target", + "rn_apple_library", + "rn_extra_build_flags", + "rn_xplat_cxx_library2", + "subdir_glob", +) +load("//tools/build_defs/third_party:yarn_defs.bzl", "yarn_workspace") + +RCTCXXBRIDGE_PUBLIC_HEADERS = { + "React/" + x: "React/CxxBridge/" + x + for x in [ + "JSCExecutorFactory.h", + "NSDataBigString.h", + "RCTCxxBridgeDelegate.h", + "RCTMessageThread.h", + ] +} + +fb_native.genrule( + name = "codegen_rn_components_schema_rncore", + srcs = glob( + [ + "Libraries/**/*NativeComponent.js", + "jest/**/*NativeComponent.js", + "packages/**/*NativeComponent.js", + ], + exclude = [ + "**/__*__/**", + + # Subfolders with their own BUCK files, referenced below + "packages/rn-tester/**", + ], + ) + [ + react_native_root_target("packages/rn-tester:nativecomponent-srcs"), + ], + cmd = "$(exe {}) $OUT $SRCS".format(react_native_root_target("packages/react-native-codegen:write_to_json")), + out = "schema-rncore.json", +) + +rn_codegen_components( + name = "rncore", + schema_target = ":codegen_rn_components_schema_rncore", +) + +rn_xplat_cxx_library2( + name = "RCTCxxBridge", + srcs = glob([ + "React/CxxBridge/*.mm", + ]), + headers = subdir_glob( + [ + ( + "React/CxxBridge", + "*.h", + ), + ], + exclude = RCTCXXBRIDGE_PUBLIC_HEADERS.values(), + prefix = "React", + ), + header_namespace = "", + exported_headers = RCTCXXBRIDGE_PUBLIC_HEADERS, + compiler_flags = [ + "-fobjc-arc-exceptions", + ], + contacts = ["oncall+react_native@xmail.facebook.com"], + exported_preprocessor_flags = rn_extra_build_flags(), + fbobjc_enable_exceptions = True, + frameworks = [ + "$SDKROOT/System/Library/Frameworks/Foundation.framework", + ], + # Used via objc_lookupClass in RCTBridge. Semantics are meant to be "if + # it's linked in your app, transparently use it". + labels = [ + "depslint_never_remove", + "supermodule:xplat/default/public.react_native.infra", + ], + preprocessor_flags = get_objc_arc_preprocessor_flags() + get_preprocessor_flags_for_build_mode() + [ + "-DWITH_FBSYSTRACE=1", + "-DRCT_USE_HERMES=0", # This is the default. + ], + visibility = ["PUBLIC"], + deps = [ + ":RCTCxxModule", + ":RCTCxxUtils", + ":ReactInternal", + "//fbobjc/Libraries/FBReactKit:RCTFBSystrace", + "//xplat/folly:molly", + react_native_root_target("React/CoreModules:CoreModules"), + react_native_xplat_target("cxxreact:bridge"), + react_native_xplat_target("cxxreact:jsbigstring"), + react_native_xplat_target("jsi:JSCRuntime"), + react_native_xplat_target("jsiexecutor:jsiexecutor"), + react_native_xplat_target("reactperflogger:reactperflogger"), + ], +) + +RCTCXXMODULE_PUBLIC_HEADERS = { + "React/" + x: "React/CxxModule/" + x + for x in [ + "RCTCxxMethod.h", + "RCTCxxModule.h", + "RCTCxxUtils.h", + ] +} + +rn_xplat_cxx_library2( + name = "RCTCxxModule", + srcs = glob([ + "React/CxxModule/*.mm", + ]), + headers = subdir_glob( + [ + ( + "React/CxxModule", + "*.h", + ), + ], + exclude = RCTCXXMODULE_PUBLIC_HEADERS.values(), + prefix = "React", + ), + header_namespace = "", + exported_headers = RCTCXXMODULE_PUBLIC_HEADERS, + compiler_flags = [ + "-fobjc-arc-exceptions", + ], + contacts = ["oncall+react_native@xmail.facebook.com"], + fbobjc_enable_exceptions = True, + frameworks = [ + "$SDKROOT/System/Library/Frameworks/Foundation.framework", + ], + labels = ["supermodule:xplat/default/public.react_native.infra"], + preprocessor_flags = get_objc_arc_preprocessor_flags() + get_preprocessor_flags_for_build_mode() + ["-DWITH_FBSYSTRACE=1"], + visibility = ["PUBLIC"], + deps = [ + ":RCTCxxUtils", + ":ReactInternal", + "//xplat/fbsystrace:fbsystrace", + "//xplat/folly:headers_only", + react_native_xplat_target("cxxreact:module"), + react_native_xplat_target("cxxreact:bridge"), + react_native_xplat_target("reactperflogger:reactperflogger"), + react_native_xplat_dep("jsi:jsi"), + ], +) + +rn_xplat_cxx_library2( + name = "RCTCxxUtils", + srcs = glob([ + "React/CxxUtils/*.mm", + ]), + header_namespace = "", + exported_headers = subdir_glob( + [ + ( + "React/CxxUtils", + "*.h", + ), + ], + exclude = RCTCXXMODULE_PUBLIC_HEADERS.values(), + prefix = "React", + ), + apple_sdks = (IOS, APPLETVOS), + appletvos_configs = fbobjc_configs(STATIC_LIBRARY_APPLETVOS_CONFIG), + appletvos_inherited_buck_flags = get_base_appletvos_flags(), + contacts = ["oncall+react_native@xmail.facebook.com"], + fbobjc_enable_exceptions = True, + frameworks = [ + "$SDKROOT/System/Library/Frameworks/Foundation.framework", + ], + labels = ["supermodule:xplat/default/public.react_native.infra"], + preprocessor_flags = get_objc_arc_preprocessor_flags() + get_preprocessor_flags_for_build_mode(), + visibility = ["PUBLIC"], + deps = [ + "//xplat/folly:molly", + ], +) + +RCTLIB_PATH = "Libraries/" + +RCTBASE_PATH = "React/Base/" + +RCTDEVSUPPORT_PATH = "React/DevSupport/" + +RCTMODULES_PATH = "React/Modules/" + +RCTVIEWS_PATH = "React/Views/" + +REACT_PUBLIC_HEADERS = { + "React/RCTAnimationType.h": RCTVIEWS_PATH + "RCTAnimationType.h", + "React/RCTAssert.h": RCTBASE_PATH + "RCTAssert.h", + "React/RCTAutoInsetsProtocol.h": RCTVIEWS_PATH + "RCTAutoInsetsProtocol.h", + "React/RCTBorderDrawing.h": RCTVIEWS_PATH + "RCTBorderDrawing.h", + "React/RCTBorderStyle.h": RCTVIEWS_PATH + "RCTBorderStyle.h", + "React/RCTBridge+Private.h": RCTBASE_PATH + "RCTBridge+Private.h", + "React/RCTBridge.h": RCTBASE_PATH + "RCTBridge.h", + "React/RCTBridgeDelegate.h": RCTBASE_PATH + "RCTBridgeDelegate.h", + "React/RCTBridgeMethod.h": RCTBASE_PATH + "RCTBridgeMethod.h", + "React/RCTBridgeModule.h": RCTBASE_PATH + "RCTBridgeModule.h", + "React/RCTBundleURLProvider.h": RCTBASE_PATH + "RCTBundleURLProvider.h", + "React/RCTComponent.h": RCTVIEWS_PATH + "RCTComponent.h", + "React/RCTComponentData.h": RCTVIEWS_PATH + "RCTComponentData.h", + "React/RCTComponentEvent.h": RCTBASE_PATH + "RCTComponentEvent.h", + "React/RCTConstants.h": RCTBASE_PATH + "RCTConstants.h", + "React/RCTConvert.h": RCTBASE_PATH + "RCTConvert.h", + "React/RCTCxxConvert.h": RCTBASE_PATH + "RCTCxxConvert.h", + "React/RCTDefines.h": RCTBASE_PATH + "RCTDefines.h", + "React/RCTDevLoadingViewProtocol.h": RCTDEVSUPPORT_PATH + "RCTDevLoadingViewProtocol.h", + "React/RCTDevLoadingViewSetEnabled.h": RCTDEVSUPPORT_PATH + "RCTDevLoadingViewSetEnabled.h", + "React/RCTDisplayLink.h": RCTBASE_PATH + "RCTDisplayLink.h", + "React/RCTErrorCustomizer.h": RCTBASE_PATH + "RCTErrorCustomizer.h", + "React/RCTErrorInfo.h": RCTBASE_PATH + "RCTErrorInfo.h", + # NOTE: RCTEventDispatcher.h is exported from CoreModules:CoreModulesApple + "React/RCTEventDispatcherProtocol.h": RCTBASE_PATH + "RCTEventDispatcherProtocol.h", + "React/RCTEventEmitter.h": RCTMODULES_PATH + "RCTEventEmitter.h", + "React/RCTFont.h": RCTVIEWS_PATH + "RCTFont.h", + "React/RCTFrameUpdate.h": RCTBASE_PATH + "RCTFrameUpdate.h", + "React/RCTI18nUtil.h": RCTMODULES_PATH + "RCTI18nUtil.h", + "React/RCTImageSource.h": RCTBASE_PATH + "RCTImageSource.h", + "React/RCTInitializing.h": RCTBASE_PATH + "RCTInitializing.h", + "React/RCTInspector.h": "React/Inspector/RCTInspector.h", + "React/RCTInspectorDevServerHelper.h": RCTDEVSUPPORT_PATH + "RCTInspectorDevServerHelper.h", + "React/RCTInspectorPackagerConnection.h": "React/Inspector/RCTInspectorPackagerConnection.h", + "React/RCTInvalidating.h": RCTBASE_PATH + "RCTInvalidating.h", + "React/RCTJSScriptLoaderModule.h": RCTBASE_PATH + "RCTJSScriptLoaderModule.h", + "React/RCTJSStackFrame.h": RCTBASE_PATH + "RCTJSStackFrame.h", + "React/RCTJavaScriptExecutor.h": RCTBASE_PATH + "RCTJavaScriptExecutor.h", + "React/RCTJavaScriptLoader.h": RCTBASE_PATH + "RCTJavaScriptLoader.h", + "React/RCTKeyCommands.h": RCTBASE_PATH + "RCTKeyCommands.h", + "React/RCTLayout.h": RCTVIEWS_PATH + "RCTLayout.h", + "React/RCTLayoutAnimation.h": RCTMODULES_PATH + "RCTLayoutAnimation.h", + "React/RCTLayoutAnimationGroup.h": RCTMODULES_PATH + "RCTLayoutAnimationGroup.h", + "React/RCTLog.h": RCTBASE_PATH + "RCTLog.h", + "React/RCTManagedPointer.h": RCTBASE_PATH + "RCTManagedPointer.h", + "React/RCTModalHostViewController.h": RCTVIEWS_PATH + "RCTModalHostViewController.h", + "React/RCTModalHostViewManager.h": RCTVIEWS_PATH + "RCTModalHostViewManager.h", + "React/RCTModalManager.h": RCTVIEWS_PATH + "RCTModalManager.h", + "React/RCTModuleData.h": RCTBASE_PATH + "RCTModuleData.h", + "React/RCTModuleMethod.h": RCTBASE_PATH + "RCTModuleMethod.h", + "React/RCTMultipartStreamReader.h": RCTBASE_PATH + "RCTMultipartStreamReader.h", + "React/RCTNullability.h": RCTBASE_PATH + "RCTNullability.h", + "React/RCTPackagerClient.h": RCTDEVSUPPORT_PATH + "RCTPackagerClient.h", + "React/RCTPackagerConnection.h": RCTDEVSUPPORT_PATH + "RCTPackagerConnection.h", + "React/RCTPerformanceLogger.h": RCTBASE_PATH + "RCTPerformanceLogger.h", + "React/RCTPointerEvents.h": RCTVIEWS_PATH + "RCTPointerEvents.h", + "React/RCTProfile.h": "React/Profiler/RCTProfile.h", + "React/RCTPushNotificationManager.h": RCTLIB_PATH + "PushNotificationIOS/RCTPushNotificationManager.h", + "React/RCTReconnectingWebSocket.h": RCTLIB_PATH + "WebSocket/RCTReconnectingWebSocket.h", + "React/RCTRedBoxExtraDataViewController.h": RCTMODULES_PATH + "RCTRedBoxExtraDataViewController.h", + "React/RCTRedBoxSetEnabled.h": RCTBASE_PATH + "RCTRedBoxSetEnabled.h", + "React/RCTRefreshableProtocol.h": RCTVIEWS_PATH + "RefreshControl/RCTRefreshableProtocol.h", + "React/RCTReloadCommand.h": RCTBASE_PATH + "RCTReloadCommand.h", + "React/RCTRootContentView.h": RCTBASE_PATH + "RCTRootContentView.h", + "React/RCTRootShadowView.h": RCTVIEWS_PATH + "RCTRootShadowView.h", + "React/RCTRootView.h": RCTBASE_PATH + "RCTRootView.h", + "React/RCTRootViewDelegate.h": RCTBASE_PATH + "RCTRootViewDelegate.h", + "React/RCTSRWebSocket.h": RCTLIB_PATH + "WebSocket/RCTSRWebSocket.h", + "React/RCTScrollEvent.h": RCTVIEWS_PATH + "ScrollView/RCTScrollEvent.h", + "React/RCTScrollView.h": RCTVIEWS_PATH + "ScrollView/RCTScrollView.h", + "React/RCTScrollableProtocol.h": RCTVIEWS_PATH + "ScrollView/RCTScrollableProtocol.h", + "React/RCTShadowView+Layout.h": RCTVIEWS_PATH + "RCTShadowView+Layout.h", + "React/RCTShadowView.h": RCTVIEWS_PATH + "RCTShadowView.h", + "React/RCTSurface.h": RCTBASE_PATH + "Surface/RCTSurface.h", + "React/RCTSurfaceDelegate.h": RCTBASE_PATH + "Surface/RCTSurfaceDelegate.h", + "React/RCTSurfaceHostingProxyRootView.h": RCTBASE_PATH + "Surface/SurfaceHostingView/RCTSurfaceHostingProxyRootView.h", + "React/RCTSurfaceHostingView.h": RCTBASE_PATH + "Surface/SurfaceHostingView/RCTSurfaceHostingView.h", + "React/RCTSurfacePresenterStub.h": RCTMODULES_PATH + "RCTSurfacePresenterStub.h", + "React/RCTSurfaceProtocol.h": RCTBASE_PATH + "Surface/RCTSurfaceProtocol.h", + "React/RCTSurfaceRootShadowView.h": RCTBASE_PATH + "Surface/RCTSurfaceRootShadowView.h", + "React/RCTSurfaceRootShadowViewDelegate.h": RCTBASE_PATH + "Surface/RCTSurfaceRootShadowViewDelegate.h", + "React/RCTSurfaceRootView.h": RCTBASE_PATH + "Surface/RCTSurfaceRootView.h", + "React/RCTSurfaceSizeMeasureMode.h": RCTBASE_PATH + "Surface/SurfaceHostingView/RCTSurfaceSizeMeasureMode.h", + "React/RCTSurfaceStage.h": RCTBASE_PATH + "Surface/RCTSurfaceStage.h", + "React/RCTSurfaceView+Internal.h": RCTBASE_PATH + "Surface/RCTSurfaceView+Internal.h", + "React/RCTSurfaceView.h": RCTBASE_PATH + "Surface/RCTSurfaceView.h", + "React/RCTTextDecorationLineType.h": RCTVIEWS_PATH + "RCTTextDecorationLineType.h", + "React/RCTTouchHandler.h": RCTBASE_PATH + "RCTTouchHandler.h", + "React/RCTUIManager.h": RCTMODULES_PATH + "RCTUIManager.h", + "React/RCTUIManagerObserverCoordinator.h": RCTMODULES_PATH + "RCTUIManagerObserverCoordinator.h", + "React/RCTUIManagerUtils.h": RCTMODULES_PATH + "RCTUIManagerUtils.h", + "React/RCTUIUtils.h": "React/UIUtils/RCTUIUtils.h", + "React/RCTURLRequestDelegate.h": RCTBASE_PATH + "RCTURLRequestDelegate.h", + "React/RCTURLRequestHandler.h": RCTBASE_PATH + "RCTURLRequestHandler.h", + "React/RCTUtils.h": RCTBASE_PATH + "RCTUtils.h", + "React/RCTUtilsUIOverride.h": RCTBASE_PATH + "RCTUtilsUIOverride.h", + "React/RCTVersion.h": RCTBASE_PATH + "RCTVersion.h", + "React/RCTView.h": RCTVIEWS_PATH + "RCTView.h", + "React/RCTViewManager.h": RCTVIEWS_PATH + "RCTViewManager.h", + "React/RCTWeakProxy.h": RCTBASE_PATH + "RCTWeakProxy.h", + "React/RCTWeakViewHolder.h": RCTVIEWS_PATH + "RCTWeakViewHolder.h", + "React/RCTWrapperViewController.h": RCTVIEWS_PATH + "RCTWrapperViewController.h", + "React/UIView+React.h": RCTVIEWS_PATH + "UIView+React.h", +} + +REACT_COMPONENTVIEWS_BASE_FILES = [ + "React/Fabric/Mounting/ComponentViews/Image/*.mm", + "React/Fabric/RCTImageResponseObserverProxy.mm", + "React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm", +] + +rn_xplat_cxx_library2( + name = "ReactInternal", + srcs = glob( + [ + "React/Base/**/*.m", + "React/Base/**/*.mm", + "React/DevSupport/**/*.m", + "React/DevSupport/**/*.mm", + "React/Inspector/**/*.m", + "React/Inspector/**/*.mm", + "React/Modules/**/*.m", + "React/Modules/**/*.mm", + "React/Profiler/**/*.m", + "React/Profiler/**/*.mm", + "React/Profiler/**/*.S", + "React/UIUtils/*.m", + "React/Views/**/*.m", + "React/Views/**/*.mm", + "Libraries/ActionSheetIOS/*.m", + "Libraries/WebSocket/*.m", + ], + ), + headers = glob( + [ + "React/Base/**/*.h", + "React/DevSupport/**/*.h", + "React/Inspector/**/*.h", + "React/Modules/**/*.h", + "React/Profiler/**/*.h", + "React/Views/**/*.h", + "React/UIUtils/**/*.h", + "Libraries/ActionSheetIOS/*.h", + "Libraries/WebSocket/*.h", + ], + ), + header_namespace = "", + exported_headers = REACT_PUBLIC_HEADERS, + compiler_flags = [ + "-Wno-error=unguarded-availability-new", + "-Wno-unknown-warning-option", + "-Wno-global-constructors", + ], + contacts = ["oncall+react_native@xmail.facebook.com"], + exported_linker_flags = [ + "-weak_framework", + "UserNotifications", + "-weak_framework", + "WebKit", + ], + exported_preprocessor_flags = rn_extra_build_flags(), + fbobjc_enable_exceptions = True, + frameworks = [ + "$SDKROOT/System/Library/Frameworks/CFNetwork.framework", + "$SDKROOT/System/Library/Frameworks/CoreGraphics.framework", + "$SDKROOT/System/Library/Frameworks/CoreLocation.framework", + "$SDKROOT/System/Library/Frameworks/Foundation.framework", + "$SDKROOT/System/Library/Frameworks/MapKit.framework", + "$SDKROOT/System/Library/Frameworks/QuartzCore.framework", + "$SDKROOT/System/Library/Frameworks/Security.framework", + "$SDKROOT/System/Library/Frameworks/SystemConfiguration.framework", + "$SDKROOT/System/Library/Frameworks/UIKit.framework", + "$SDKROOT/System/Library/Frameworks/UserNotifications.framework", + ], + labels = [ + "depslint_never_add", + "depslint_never_remove", # Some old NativeModule still relies on +load unfortunately. + "supermodule:xplat/default/public.react_native.infra", + ], + platform_preprocessor_flags = [( + "linux", + ["-D PIC_MODIFIER=@PLT"], + )], + preprocessor_flags = get_objc_arc_preprocessor_flags() + get_preprocessor_flags_for_build_mode() + [ + "-DHERMES_BYTECODE_VERSION={}".format(HERMES_BYTECODE_VERSION), + ] + rn_extra_build_flags(), + visibility = [ + "//fbobjc/Apps/Internal/SparkLabs/...", + "//fbobjc/Apps/Internal/Venice/...", + "//fbobjc/Apps/Wilde/FBMarketplaceModule/...", + "//fbobjc/Apps/Wilde/FBReactModule2/...", + "//fbobjc/Libraries/FBQPLMetadataProviders/...", + "//fbobjc/Libraries/FBReactKit/...", + "//fbobjc/Libraries/FBiOSSecurityUtils/...", + "//fbobjc/VendorLib/react-native-maps:react-native-maps", + "//xplat/js:", + "//xplat/js/react-native-github/React/...", + "//xplat/js/react-native-github/ReactCommon/react/nativemodule/core:", + "//xplat/js/react-native-github/ReactCommon/react/nativemodule/samples:", + "//xplat/js/react-native-github/packages/rn-tester:", + ], + deps = [ + YOGA_CXX_TARGET, + react_native_xplat_target("cxxreact:bridge"), + react_native_xplat_target("reactperflogger:reactperflogger"), + ], +) + +rn_xplat_cxx_library2( + name = "RCTFabric", + srcs = glob( + [ + "React/Fabric/**/*.cpp", + "React/Fabric/**/*.m", + "React/Fabric/**/*.mm", + ], + exclude = glob(REACT_COMPONENTVIEWS_BASE_FILES), + ), + headers = glob( + [ + "React/Fabric/**/*.h", + ], + ), + header_namespace = "", + exported_headers = { + "React/RCTComponentViewDescriptor.h": "React/Fabric/Mounting/RCTComponentViewDescriptor.h", + "React/RCTComponentViewFactory.h": "React/Fabric/Mounting/RCTComponentViewFactory.h", + "React/RCTComponentViewRegistry.h": "React/Fabric/Mounting/RCTComponentViewRegistry.h", + "React/RCTFabricSurface.h": "React/Fabric/Surface/RCTFabricSurface.h", + "React/RCTFabricSurfaceHostingProxyRootView.h": "React/Fabric/Surface/RCTFabricSurfaceHostingProxyRootView.h", + "React/RCTFabricSurfaceHostingView.h": "React/Fabric/Surface/RCTFabricSurfaceHostingView.h", + "React/RCTGenericDelegateSplitter.h": "React/Fabric/Utils/RCTGenericDelegateSplitter.h", + "React/RCTLegacyViewManagerInteropComponentView.h": "React/Fabric/Mounting/ComponentViews/LegacyViewManagerInterop/RCTLegacyViewManagerInteropComponentView.h", + "React/RCTLocalizationProvider.h": "React/Fabric/RCTLocalizationProvider.h", + "React/RCTModalHostViewComponentView.h": "React/Fabric/Mounting/ComponentViews/Modal/RCTModalHostViewComponentView.h", + "React/RCTMountingManager.h": "React/Fabric/Mounting/RCTMountingManager.h", + "React/RCTMountingManagerDelegate.h": "React/Fabric/Mounting/RCTMountingManagerDelegate.h", + "React/RCTMountingTransactionObserving.h": "React/Fabric/Mounting/RCTMountingTransactionObserving.h", + "React/RCTParagraphComponentAccessibilityProvider.h": "React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentAccessibilityProvider.h", + "React/RCTParagraphComponentView.h": "React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.h", + "React/RCTPrimitives.h": "React/Fabric/RCTPrimitives.h", + "React/RCTRootComponentView.h": "React/Fabric/Mounting/ComponentViews/Root/RCTRootComponentView.h", + "React/RCTScheduler.h": "React/Fabric/RCTScheduler.h", + "React/RCTScrollViewComponentView.h": "React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.h", + "React/RCTSurfacePresenter.h": "React/Fabric/RCTSurfacePresenter.h", + "React/RCTSurfacePresenterBridgeAdapter.h": "React/Fabric/RCTSurfacePresenterBridgeAdapter.h", + "React/RCTSurfaceRegistry.h": "React/Fabric/RCTSurfaceRegistry.h", + "React/RCTSurfaceTouchHandler.h": "React/Fabric/RCTSurfaceTouchHandler.h", + }, + compiler_flags = [ + "-fexceptions", + "-frtti", + "-std=c++17", + "-Wall", + ], + contacts = ["oncall+react_native@xmail.facebook.com"], + fbobjc_enable_exceptions = True, + fbobjc_target_sdk_version = "11.0", + frameworks = [ + "$SDKROOT/System/Library/Frameworks/Foundation.framework", + "$SDKROOT/System/Library/Frameworks/QuartzCore.framework", + "$SDKROOT/System/Library/Frameworks/UIKit.framework", + ], + header_path_prefix = "React", + labels = [ + "disable_plugins_only_validation", + "supermodule:xplat/default/public.react_native.infra", + ], + plugins = [ + react_fabric_component_plugin_provider("SafeAreaView", "RCTSafeAreaViewCls"), + react_fabric_component_plugin_provider("ScrollView", "RCTScrollViewCls"), + react_fabric_component_plugin_provider("PullToRefreshView", "RCTPullToRefreshViewCls"), + react_fabric_component_plugin_provider("ActivityIndicatorView", "RCTActivityIndicatorViewCls"), + react_fabric_component_plugin_provider("Slider", "RCTSliderCls"), + react_fabric_component_plugin_provider("Switch", "RCTSwitchCls"), + react_fabric_component_plugin_provider("UnimplementedNativeView", "RCTUnimplementedNativeViewCls"), + react_fabric_component_plugin_provider("Paragraph", "RCTParagraphCls"), + react_fabric_component_plugin_provider("TextInput", "RCTTextInputCls"), + react_fabric_component_plugin_provider("InputAccessoryView", "RCTInputAccessoryCls"), + react_fabric_component_plugin_provider("View", "RCTViewCls"), + ], + plugins_header = "FBRCTFabricComponentsPlugins.h", + preprocessor_flags = get_objc_arc_preprocessor_flags() + get_preprocessor_flags_for_build_mode() + [ + "-DWITH_FBSYSTRACE=1", + "-DLOG_TAG=\"ReactNative\"", + "-DRN_DISABLE_OSS_PLUGIN_HEADER", + ] + rn_extra_build_flags(), + tests = [ + ":MountingTests", + ":TextTests", + ], + visibility = ["PUBLIC"], + deps = [ + ":RCTFabricComponentViewsBase", + "//fbobjc/Libraries/FBReactKit/RCTFabricComponent/RCTFabricComponentPlugin:RCTFabricComponentPlugin", + "//xplat/js/react-native-github:RCTCxxBridge", + "//xplat/js/react-native-github:RCTCxxUtils", + "//xplat/js/react-native-github:RCTImage", + "//xplat/js/react-native-github:RCTPushNotification", + "//xplat/js/react-native-github:RCTText", + "//xplat/js/react-native-github:ReactInternal", + react_native_xplat_target("react/renderer/attributedstring:attributedstring"), + react_native_xplat_target("react/renderer/componentregistry:componentregistry"), + react_native_xplat_target("react/renderer/componentregistry/native:native"), + react_native_xplat_target("react/renderer/textlayoutmanager:textlayoutmanager"), + react_native_xplat_target("runtimeexecutor:runtimeexecutor"), + YOGA_CXX_TARGET, + react_native_xplat_target("react/config:config"), + react_native_xplat_target("cxxreact:bridge"), + ], + exported_deps = [ + react_native_xplat_target("react/renderer/animations:animations"), + react_native_xplat_target("react/renderer/components/scrollview:scrollview"), + react_native_xplat_target("react/renderer/components/slider:slider"), + react_native_xplat_target("react/renderer/components/safeareaview:safeareaview"), + react_native_xplat_target("react/renderer/components/modal:modal"), + react_native_xplat_target("react/renderer/components/unimplementedview:unimplementedview"), + react_native_xplat_target("react/renderer/components/text:text"), + react_native_xplat_target("react/renderer/components/legacyviewmanagerinterop:legacyviewmanagerinterop"), + react_native_xplat_target("react/renderer/components/textinput/iostextinput:iostextinput"), + react_native_xplat_target("react/renderer/components/inputaccessory:inputaccessory"), + react_native_xplat_target("react/renderer/core:core"), + react_native_xplat_target("react/renderer/debug:debug"), + react_native_xplat_target("react/renderer/scheduler:scheduler"), + react_native_xplat_target("react/renderer/uimanager:uimanager"), + "//xplat/js/react-native-github:generated_components-rncore", + ], +) + +rn_apple_library( + name = "RCTTypeSafety", + srcs = glob([ + "Libraries/TypeSafety/**/*.mm", + ]), + exported_headers = glob( + [ + "Libraries/TypeSafety/**/*.h", + ], + ), + autoglob = False, + contacts = ["oncall+react_native@xmail.facebook.com"], + extension_api_only = True, + frameworks = [ + "$PLATFORM_DIR/Developer/Library/Frameworks/Foundation.framework", + ], + inherited_buck_flags = get_static_library_ios_flags(), + labels = ["supermodule:xplat/default/public.react_native.infra"], + preprocessor_flags = get_objc_arc_preprocessor_flags() + get_preprocessor_flags_for_build_mode(), + reexport_all_header_dependencies = True, + deps = [ + ":ReactInternalApple", + "//xplat/folly:optionalApple", + "//xplat/js/react-native-github/Libraries/FBLazyVector:FBLazyVector", + ], +) + +yarn_workspace( + name = "yarn-workspace", + srcs = [ + "package.json", + ], + visibility = ["PUBLIC"], +) + +fb_apple_test( + name = "TextTestsApple", + srcs = ["React/Tests/Text/RCTParagraphComponentViewTests.mm"], + frameworks = [ + "$PLATFORM_DIR/Developer/Library/Frameworks/XCTest.framework", + ], + oncall = "react_native", + deps = [ + ":RCTFabricApple", + react_native_xplat_target("react/renderer/element:elementApple"), + "//xplat/js/react-native-github:RCTFabricComponentViewsBaseApple", + "//xplat/js/react-native-github:RCTTextApple", + "//xplat/js/react-native-github/ReactCommon/react/renderer/attributedstring:attributedstringApple", + "//xplat/js/react-native-github/ReactCommon/react/renderer/componentregistry:componentregistryApple", + "//xplat/js/react-native-github/ReactCommon/react/renderer/components/legacyviewmanagerinterop:legacyviewmanagerinteropApple", + "//xplat/js/react-native-github/ReactCommon/react/renderer/components/text:textApple", + "//xplat/js/react-native-github/ReactCommon/react/renderer/components/textinput/iostextinput:iostextinputApple", + "//xplat/js/react-native-github/ReactCommon/react/renderer/scheduler:schedulerApple", + "//xplat/js/react-native-github/ReactCommon/react/renderer/textlayoutmanager:textlayoutmanagerApple", + "//xplat/js/react-native-github/ReactCommon/runtimeexecutor:runtimeexecutorApple", + ], +) + +fb_apple_test( + name = "MountingTestsApple", + srcs = ["React/Tests/Mounting/RCTComponentViewRegistryTests.mm"], + frameworks = [ + "$PLATFORM_DIR/Developer/Library/Frameworks/XCTest.framework", + ], + oncall = "react_native", + deps = [ + ":ImageView", + ":RCTFabricApple", + "//xplat/js/react-native-github:RCTFabricComponentViewsBaseApple", + "//xplat/js/react-native-github:RCTTextApple", + "//xplat/js/react-native-github/ReactCommon/react/renderer/attributedstring:attributedstringApple", + "//xplat/js/react-native-github/ReactCommon/react/renderer/componentregistry:componentregistryApple", + "//xplat/js/react-native-github/ReactCommon/react/renderer/components/legacyviewmanagerinterop:legacyviewmanagerinteropApple", + "//xplat/js/react-native-github/ReactCommon/react/renderer/components/textinput/iostextinput:iostextinputApple", + "//xplat/js/react-native-github/ReactCommon/react/renderer/scheduler:schedulerApple", + "//xplat/js/react-native-github/ReactCommon/react/renderer/textlayoutmanager:textlayoutmanagerApple", + "//xplat/js/react-native-github/ReactCommon/runtimeexecutor:runtimeexecutorApple", + ], +) + +rn_apple_library( + name = "ImageView", + autoglob = False, + compiler_flags = ["-Wall"], + contacts = ["oncall+react_native@xmail.facebook.com"], + labels = [ + "disable_plugins_only_validation", + "supermodule:xplat/default/public.react_native.infra", + ], + plugins = [react_fabric_component_plugin_provider("Image", "RCTImageCls")], + visibility = ["PUBLIC"], + exported_deps = [ + ":RCTFabricComponentViewsBaseApple", + ], +) + +# Reduce the RCTFabric target by moving OSS RCTViewComponentViews here, so that +# eventually we can move all of React/Fabric/Mounting/ComponentViews/* here. +# Ideally, each component view gets its own target, and each target uses react_fabric_component_plugin_provider. +# For each component, an app can import the base component view, or an app-specific subclass. +# i.e. Apps depend on "ImageView" target for RCTImageComponentView.h, and "FBReactImageView" target for FBReactImageComponentView.h +rn_xplat_cxx_library2( + name = "RCTFabricComponentViewsBase", + srcs = glob(REACT_COMPONENTVIEWS_BASE_FILES), + header_namespace = "", + exported_headers = { + "React/RCTComponentViewProtocol.h": "React/Fabric/Mounting/RCTComponentViewProtocol.h", + "React/RCTConversions.h": "React/Fabric/RCTConversions.h", + "React/RCTImageComponentView.h": "React/Fabric/Mounting/ComponentViews/Image/RCTImageComponentView.h", + "React/RCTImageResponseDelegate.h": "React/Fabric/RCTImageResponseDelegate.h", + "React/RCTImageResponseObserverProxy.h": "React/Fabric/RCTImageResponseObserverProxy.h", + "React/RCTTouchableComponentViewProtocol.h": "React/Fabric/RCTTouchableComponentViewProtocol.h", + "React/RCTViewComponentView.h": "React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.h", + "React/UIView+ComponentViewProtocol.h": "React/Fabric/Mounting/UIView+ComponentViewProtocol.h", + }, + compiler_flags = ["-Wall"], + contacts = ["oncall+react_native@xmail.facebook.com"], + labels = ["supermodule:xplat/default/public.react_native.infra"], + visibility = ["PUBLIC"], + deps = [ + "//xplat/js/react-native-github:RCTImage", + "//xplat/js/react-native-github:RCTLinking", + react_native_xplat_target("react/renderer/imagemanager:imagemanager"), + react_native_xplat_target("react/renderer/components/image:image"), + react_native_xplat_target("react/renderer/components/view:view"), + react_native_xplat_target("react/renderer/componentregistry:componentregistry"), + ], +) + +rn_library( + name = "react-native", + srcs = [ + "package.json", + "index.js", + ] + glob( + [ + "Libraries/**/*.js", + "Libraries/NewAppScreen/**/*.png", + "Libraries/LogBox/**/*.png", + ], + exclude = [ + "**/__*__/**", + "**/gulpfile.js", + "Libraries/Components/Switch/SwitchSchema.js", + ], + ), + labels = ["supermodule:xplat/default/public.react_native.core"], + skip_processors = True, # Don't anticipate routes or fbicon here + visibility = ["PUBLIC"], + deps = [ + "//xplat/js:node_modules__abort_19controller", + "//xplat/js:node_modules__anser", + "//xplat/js:node_modules__base64_19js", + "//xplat/js:node_modules__event_19target_19shim", + "//xplat/js:node_modules__invariant", + "//xplat/js:node_modules__nullthrows", + "//xplat/js:node_modules__pretty_19format", + "//xplat/js:node_modules__promise", + "//xplat/js:node_modules__prop_19types", + "//xplat/js:node_modules__react_19devtools_19core", + "//xplat/js:node_modules__react_19refresh", + "//xplat/js:node_modules__react_19shallow_19renderer", + "//xplat/js:node_modules__regenerator_19runtime", + "//xplat/js:node_modules__stacktrace_19parser", + "//xplat/js:node_modules__use_19subscription", + "//xplat/js:node_modules__whatwg_19fetch", + "//xplat/js/RKJSModules/Libraries/Polyfills:Polyfills", + "//xplat/js/RKJSModules/Libraries/React:React", + "//xplat/js/RKJSModules/vendor/react:react", + "//xplat/js/RKJSModules/vendor/react-test-renderer:react-test-renderer", + "//xplat/js/RKJSModules/vendor/scheduler:scheduler", + "//xplat/js/react-native-github/packages/assets:assets", + "//xplat/js/react-native-github/packages/normalize-color:normalize-color", + "//xplat/js/react-native-github/packages/polyfills:polyfills", + "//xplat/js/tools/metro/packages/metro-runtime/src/modules:modules", + "//xplat/js/tools/metro/packages/metro-runtime/src/polyfills:polyfills", + ], +) + +rn_codegen( + name = "FBReactNativeSpec", + android_package_name = "com.facebook.fbreact.specs", + codegen_modules = True, + ios_assume_nonnull = False, + library_labels = ["supermodule:xplat/default/public.react_native.infra"], + native_module_spec_name = "FBReactNativeSpec", + src_prefix = "Libraries/", +) + +# TODO: Merge this into FBReactNativeSpec +rn_codegen( + name = "FBReactNativeComponentSpec", + codegen_components = True, + ios_assume_nonnull = False, + library_labels = ["supermodule:xplat/default/public.react_native.infra"], + src_prefix = "Libraries/", +) + +rn_apple_library( + name = "RCTAnimationApple", + srcs = glob([ + "Libraries/NativeAnimation/**/*.m", + "Libraries/NativeAnimation/**/*.mm", + ]), + headers = glob( + [ + "Libraries/NativeAnimation/**/*.h", + ], + ), + header_namespace = "", + exported_headers = glob( + [ + "Libraries/NativeAnimation/*.h", + "Libraries/NativeAnimation/Drivers/*.h", + "Libraries/NativeAnimation/Nodes/*.h", + ], + ), + autoglob = False, + frameworks = [ + "Foundation", + "QuartzCore", + "UIKit", + ], + header_path_prefix = "React", + labels = [ + "depslint_never_remove", # Some old NativeModule still relies on +load unfortunately. + "disable_plugins_only_validation", + "extension_api_allow_unsafe_unavailable_usages", + "supermodule:xplat/default/public.react_native.infra", + ], + plugins = + react_module_plugin_providers( + name = "NativeAnimatedModule", + native_class_func = "RCTNativeAnimatedModuleCls", + ) + react_module_plugin_providers( + name = "NativeAnimatedTurboModule", + native_class_func = "RCTNativeAnimatedTurboModuleCls", + ), + plugins_header = "FBRCTAnimationPlugins.h", + preprocessor_flags = get_objc_arc_preprocessor_flags() + get_preprocessor_flags_for_build_mode() + rn_extra_build_flags() + [ + "-DRN_DISABLE_OSS_PLUGIN_HEADER", + ], + visibility = ["PUBLIC"], + deps = [ + "//xplat/js/react-native-github:FBReactNativeSpecApple", + "//xplat/js/react-native-github:RCTLinkingApple", + "//xplat/js/react-native-github:RCTPushNotificationApple", + "//xplat/js/react-native-github:ReactInternalApple", + ], +) + +rn_apple_library( + name = "RCTBlobApple", + srcs = glob([ + "Libraries/Blob/*.m", + "Libraries/Blob/*.mm", + ]), + headers = glob( + [ + "Libraries/Blob/*.h", + ], + ), + exported_headers = glob( + [ + "Libraries/Blob/*.h", + ], + ), + autoglob = False, + enable_exceptions = True, + frameworks = [ + "Foundation", + "UIKit", + ], + header_path_prefix = "React", + labels = [ + "depslint_never_remove", # Some old NativeModule still relies on +load unfortunately. + "disable_plugins_only_validation", + "supermodule:xplat/default/public.react_native.infra", + ], + plugins = + react_module_plugin_providers( + name = "FileReaderModule", + native_class_func = "RCTFileReaderModuleCls", + ) + react_module_plugin_providers( + name = "BlobModule", + native_class_func = "RCTBlobManagerCls", + ) + [ + plugin( + RCT_URL_REQUEST_HANDLER_SOCKET, + name = "BlobModule", + ), + ], + plugins_header = "FBRCTBlobPlugins.h", + preprocessor_flags = get_objc_arc_preprocessor_flags() + get_preprocessor_flags_for_build_mode() + rn_extra_build_flags() + [ + "-DRN_DISABLE_OSS_PLUGIN_HEADER", + ], + visibility = ["PUBLIC"], + deps = [ + ":RCTNetworkApple", + "//xplat/js/react-native-github:FBReactNativeSpecApple", + "//xplat/js/react-native-github:RCTLinkingApple", + "//xplat/js/react-native-github:RCTPushNotificationApple", + "//xplat/js/react-native-github:ReactInternalApple", + "//xplat/js/react-native-github/React/CoreModules:CoreModulesApple", + "//xplat/jsi:jsiApple", + ], +) + +rn_apple_library( + name = "RCTLinkingApple", + srcs = glob([ + "Libraries/LinkingIOS/*.m", + "Libraries/LinkingIOS/*.mm", + ]), + headers = glob( + [ + "Libraries/LinkingIOS/*.h", + ], + ), + exported_headers = glob( + [ + "Libraries/LinkingIOS/*.h", + ], + ), + autoglob = False, + enable_exceptions = True, + frameworks = [ + "Foundation", + "UIKit", + ], + header_path_prefix = "React", + labels = [ + "depslint_never_remove", # Some old NativeModule still relies on +load unfortunately. + "disable_plugins_only_validation", + "extension_api_allow_unsafe_unavailable_usages", + "supermodule:xplat/default/public.react_native.infra", + ], + plugins = + react_module_plugin_providers( + name = "LinkingManager", + native_class_func = "RCTLinkingManagerCls", + ), + plugins_header = "FBRCTLinkingPlugins.h", + preprocessor_flags = get_objc_arc_preprocessor_flags() + get_preprocessor_flags_for_build_mode() + rn_extra_build_flags() + [ + "-DRN_DISABLE_OSS_PLUGIN_HEADER", + ], + visibility = ["PUBLIC"], + deps = [ + "//xplat/js/react-native-github:FBReactNativeSpecApple", + "//xplat/js/react-native-github:RCTPushNotificationApple", + "//xplat/js/react-native-github:ReactInternalApple", + "//xplat/jsi:jsiApple", + ], +) + +rn_apple_library( + name = "RCTPushNotificationApple", + srcs = glob([ + "Libraries/PushNotificationIOS/*.m", + "Libraries/PushNotificationIOS/*.mm", + ]), + headers = glob( + [ + "Libraries/PushNotificationIOS/*.h", + ], + ), + exported_headers = glob( + [ + "Libraries/PushNotificationIOS/*.h", + ], + ), + autoglob = False, + enable_exceptions = True, + frameworks = [ + "Foundation", + "UIKit", + ], + header_path_prefix = "React", + labels = [ + "depslint_never_remove", # Some old NativeModule still relies on +load unfortunately. + "disable_plugins_only_validation", + "extension_api_allow_unsafe_unavailable_usages", + "supermodule:xplat/default/public.react_native.infra", + ], + plugins = + react_module_plugin_providers( + name = "PushNotificationManager", + native_class_func = "RCTPushNotificationManagerCls", + ), + plugins_header = "FBRCTPushNotificationPlugins.h", + preprocessor_flags = get_objc_arc_preprocessor_flags() + get_preprocessor_flags_for_build_mode() + rn_extra_build_flags() + [ + "-DRN_DISABLE_OSS_PLUGIN_HEADER", + ], + visibility = ["PUBLIC"], + deps = [ + "//xplat/js/react-native-github:FBReactNativeSpecApple", + "//xplat/js/react-native-github:ReactInternalApple", + "//xplat/jsi:jsiApple", + ], +) + +rn_apple_library( + name = "RCTImageApple", + srcs = glob([ + "Libraries/Image/*.m", + "Libraries/Image/*.mm", + ]), + headers = glob( + [ + "Libraries/Image/*.h", + ], + ), + exported_headers = glob( + [ + "Libraries/Image/*.h", + ], + ), + autoglob = False, + frameworks = [ + "AVFoundation", + "Accelerate", + "CoreMedia", + "Foundation", + "ImageIO", + "MobileCoreServices", + "QuartzCore", + "UIKit", + ], + header_path_prefix = "React", + labels = [ + "depslint_never_remove", # Some old NativeModule still relies on +load unfortunately. + "disable_plugins_only_validation", + "extension_api_allow_unsafe_unavailable_usages", + "supermodule:xplat/default/public.react_native.infra", + ], + plugins = + react_module_plugin_providers( + name = "GIFImageDecoder", + native_class_func = "RCTGIFImageDecoderCls", + ) + react_module_plugin_providers( + name = "ImageEditingManager", + native_class_func = "RCTImageEditingManagerCls", + ) + react_module_plugin_providers( + name = "ImageLoader", + native_class_func = "RCTImageLoaderCls", + ) + react_module_plugin_providers( + name = "ImageStoreManager", + native_class_func = "RCTImageStoreManagerCls", + ) + react_module_plugin_providers( + name = "LocalAssetImageLoader", + native_class_func = "RCTLocalAssetImageLoaderCls", + ) + [ + plugin( + RCT_IMAGE_DATA_DECODER_SOCKET, + name = "GIFImageDecoder", + ), + plugin( + RCT_IMAGE_URL_LOADER_SOCKET, + name = "LocalAssetImageLoader", + ), + plugin( + RCT_URL_REQUEST_HANDLER_SOCKET, + name = "ImageLoader", + ), + plugin( + RCT_URL_REQUEST_HANDLER_SOCKET, + name = "ImageStoreManager", + ), + ], + plugins_header = "FBRCTImagePlugins.h", + preprocessor_flags = get_objc_arc_preprocessor_flags() + get_preprocessor_flags_for_build_mode() + rn_extra_build_flags() + [ + "-DRN_DISABLE_OSS_PLUGIN_HEADER", + ], + visibility = ["PUBLIC"], + deps = [ + ":RCTNetworkApple", + "//xplat/js/react-native-github:FBReactNativeSpecApple", + "//xplat/js/react-native-github:RCTLinkingApple", + "//xplat/js/react-native-github:RCTPushNotificationApple", + "//xplat/js/react-native-github:ReactInternalApple", + ], +) + +RCTNETWORK_PUBLIC_HEADERS = [ + "Libraries/Network/RCTNetworkTask.h", + "Libraries/Network/RCTNetworking.h", +] + +rn_apple_library( + name = "RCTNetworkApple", + srcs = glob([ + "Libraries/Network/*.m", + "Libraries/Network/*.mm", + ]), + headers = glob( + [ + "Libraries/Network/*.h", + ], + exclude = RCTNETWORK_PUBLIC_HEADERS, + ), + exported_headers = RCTNETWORK_PUBLIC_HEADERS, + autoglob = False, + enable_exceptions = True, + frameworks = [ + "CoreTelephony", + "Foundation", + "MobileCoreServices", + ], + header_path_prefix = "React", + labels = [ + "depslint_never_remove", # Some old NativeModule still relies on +load unfortunately. + "disable_plugins_only_validation", + "extension_api_allow_unsafe_unavailable_usages", + "supermodule:xplat/default/public.react_native.infra", + ], + plugins = + react_module_plugin_providers( + name = "Networking", + native_class_func = "RCTNetworkingCls", + ) + react_module_plugin_providers( + name = "DataRequestHandler", + native_class_func = "RCTDataRequestHandlerCls", + ) + react_module_plugin_providers( + name = "FileRequestHandler", + native_class_func = "RCTFileRequestHandlerCls", + ) + react_module_plugin_providers( + name = "HTTPRequestHandler", + native_class_func = "RCTHTTPRequestHandlerCls", + ) + [ + plugin( + RCT_URL_REQUEST_HANDLER_SOCKET, + name = "DataRequestHandler", + ), + plugin( + RCT_URL_REQUEST_HANDLER_SOCKET, + name = "FileRequestHandler", + ), + plugin( + RCT_URL_REQUEST_HANDLER_SOCKET, + name = "HTTPRequestHandler", + ), + ], + plugins_header = "FBRCTNetworkPlugins.h", + preprocessor_flags = get_objc_arc_preprocessor_flags() + get_preprocessor_flags_for_build_mode() + rn_extra_build_flags() + [ + "-DRN_DISABLE_OSS_PLUGIN_HEADER", + ], + visibility = ["PUBLIC"], + deps = [ + "//xplat/js/react-native-github:FBReactNativeSpecApple", + "//xplat/js/react-native-github:RCTLinkingApple", + "//xplat/js/react-native-github:RCTPushNotificationApple", + "//xplat/js/react-native-github:ReactInternalApple", + ], +) + +rn_apple_library( + name = "RCTSettingsApple", + srcs = glob([ + "Libraries/Settings/*.m", + "Libraries/Settings/*.mm", + ]), + exported_headers = glob( + [ + "Libraries/Settings/*.h", + ], + ), + autoglob = False, + frameworks = [ + "Foundation", + ], + header_path_prefix = "React", + labels = [ + "depslint_never_remove", # Some old NativeModule still relies on +load unfortunately. + "disable_plugins_only_validation", + ], + plugins = react_module_plugin_providers( + name = "SettingsManager", + native_class_func = "RCTSettingsManagerCls", + ), + plugins_header = "FBRCTSettingsPlugins.h", + preprocessor_flags = get_objc_arc_preprocessor_flags() + get_preprocessor_flags_for_build_mode() + rn_extra_build_flags() + [ + "-DRN_DISABLE_OSS_PLUGIN_HEADER", + ], + visibility = ["PUBLIC"], + deps = [ + "//xplat/js/react-native-github:FBReactNativeSpecApple", + "//xplat/js/react-native-github:RCTLinkingApple", + "//xplat/js/react-native-github:RCTPushNotificationApple", + "//xplat/js/react-native-github:ReactInternalApple", + ], +) + +rn_xplat_cxx_library2( + name = "RCTText", + srcs = glob([ + "Libraries/Text/**/*.m", + "Libraries/Text/**/*.mm", + ]), + headers = glob( + [ + "Libraries/Text/**/*.h", + ], + ), + header_namespace = "", + exported_headers = subdir_glob( + [ + ( + "Libraries/Text", + "*.h", + ), + ( + "Libraries/Text/BaseText", + "*.h", + ), + ( + "Libraries/Text/RawText", + "*.h", + ), + ( + "Libraries/Text/Text", + "*.h", + ), + ( + "Libraries/Text/TextInput", + "*.h", + ), + ( + "Libraries/Text/TextInput/Multiline", + "*.h", + ), + ( + "Libraries/Text/TextInput/Singleline", + "*.h", + ), + ( + "Libraries/Text/VirtualText", + "*.h", + ), + ], + prefix = "React", + ), + frameworks = [ + "$SDKROOT/System/Library/Frameworks/UIKit.framework", + ], + labels = [ + "depslint_never_remove", # Some old NativeModule still relies on +load unfortunately. + "supermodule:xplat/default/public.react_native.infra", + ], + preprocessor_flags = get_objc_arc_preprocessor_flags() + get_preprocessor_flags_for_build_mode(), + visibility = ["PUBLIC"], + deps = [ + "//xplat/js/react-native-github:RCTLinking", + "//xplat/js/react-native-github:RCTPushNotification", + "//xplat/js/react-native-github:ReactInternal", + YOGA_CXX_TARGET, + ], +) + +rn_apple_library( + name = "RCTVibrationApple", + srcs = glob([ + "Libraries/Vibration/**/*.m", + "Libraries/Vibration/**/*.mm", + ]), + exported_headers = glob( + [ + "Libraries/Vibration/*.h", + ], + ), + autoglob = False, + frameworks = [ + "AudioToolbox", + "Foundation", + ], + header_path_prefix = "React", + labels = [ + "depslint_never_remove", + "disable_plugins_only_validation", + "supermodule:xplat/default/public.react_native.infra", + ], + plugins = react_module_plugin_providers( + name = "Vibration", + native_class_func = "RCTVibrationCls", + ), + plugins_header = "FBRCTVibrationPlugins.h", + preprocessor_flags = get_objc_arc_preprocessor_flags() + get_preprocessor_flags_for_build_mode() + rn_extra_build_flags() + [ + "-DRN_DISABLE_OSS_PLUGIN_HEADER", + ], + visibility = ["PUBLIC"], + deps = [ + "//xplat/js/react-native-github:FBReactNativeSpecApple", + "//xplat/js/react-native-github:RCTLinkingApple", + "//xplat/js/react-native-github:RCTPushNotificationApple", + "//xplat/js/react-native-github:ReactInternalApple", + ], +) + +rn_xplat_cxx_library2( + name = "RCTWrapper", + srcs = glob([ + "Libraries/Wrapper/*.m", + "Libraries/Wrapper/*.mm", + ]), + header_namespace = "", + exported_headers = subdir_glob( + [ + ( + "Libraries/Wrapper", + "*.h", + ), + ], + prefix = "RCTWrapper", + ), + frameworks = [ + "$SDKROOT/System/Library/Frameworks/Foundation.framework", + ], + labels = [ + "depslint_never_remove", # Some old NativeModule still relies on +load unfortunately. + "supermodule:xplat/default/public.react_native.infra", + ], + preprocessor_flags = get_objc_arc_preprocessor_flags() + get_preprocessor_flags_for_build_mode(), + visibility = ["PUBLIC"], + deps = [ + "//xplat/js/react-native-github:RCTLinking", + "//xplat/js/react-native-github:RCTPushNotification", + "//xplat/js/react-native-github:ReactInternal", + ], +) + +rn_xplat_cxx_library2( + name = "RCTWrapperExample", + srcs = glob([ + "Libraries/Wrapper/Example/*.m", + "Libraries/Wrapper/Example/*.mm", + ]), + header_namespace = "", + exported_headers = subdir_glob( + [ + ( + "Libraries/Wrapper/Example", + "*.h", + ), + ], + prefix = "RCTWrapperExample", + ), + frameworks = [ + "$SDKROOT/System/Library/Frameworks/Foundation.framework", + ], + labels = [ + "depslint_never_remove", + "supermodule:xplat/default/public.react_native.infra", + ], + preprocessor_flags = get_objc_arc_preprocessor_flags() + get_preprocessor_flags_for_build_mode(), + visibility = ["PUBLIC"], + deps = [ + ":RCTWrapper", + "//xplat/js/react-native-github:RCTLinking", + "//xplat/js/react-native-github:RCTPushNotification", + "//xplat/js/react-native-github:ReactInternal", + ], +) + +rn_xplat_cxx_library2( + name = "RCTSurfaceHostingComponent", + srcs = glob([ + "Libraries/SurfaceHostingComponent/**/*.m", + "Libraries/SurfaceHostingComponent/**/*.mm", + ]), + header_namespace = "", + exported_headers = subdir_glob( + [ + ( + "Libraries/SurfaceHostingComponent", + "*.h", + ), + ], + prefix = "RCTSurfaceHostingComponent", + ), + frameworks = [ + "$SDKROOT/System/Library/Frameworks/Foundation.framework", + "$SDKROOT/System/Library/Frameworks/UIKit.framework", + ], + labels = [ + "depslint_never_remove", + "supermodule:xplat/default/public.react_native.infra", + ], + preprocessor_flags = get_objc_arc_preprocessor_flags() + get_preprocessor_flags_for_build_mode(), + visibility = ["PUBLIC"], + deps = [ + "//fbobjc/Libraries/MobileUI/ComponentKit:ComponentKit", + "//xplat/js/react-native-github:RCTLinking", + "//xplat/js/react-native-github:RCTPushNotification", + "//xplat/js/react-native-github:ReactInternal", + ], +) + +rn_xplat_cxx_library2( + name = "RCTSurfaceBackedComponent", + srcs = glob([ + "Libraries/SurfaceBackedComponent/**/*.m", + "Libraries/SurfaceBackedComponent/**/*.mm", + ]), + header_namespace = "", + exported_headers = subdir_glob( + [ + ( + "Libraries/SurfaceBackedComponent", + "*.h", + ), + ], + prefix = "RCTSurfaceBackedComponent", + ), + frameworks = [ + "$SDKROOT/System/Library/Frameworks/Foundation.framework", + "$SDKROOT/System/Library/Frameworks/UIKit.framework", + ], + labels = [ + "depslint_never_remove", + "supermodule:xplat/default/public.react_native.infra", + ], + preprocessor_flags = get_objc_arc_preprocessor_flags() + get_preprocessor_flags_for_build_mode(), + visibility = ["PUBLIC"], + deps = [ + ":RCTSurfaceHostingComponent", + "//fbobjc/Libraries/MobileUI/ComponentKit:ComponentKit", + "//xplat/js/react-native-github:RCTLinking", + "//xplat/js/react-native-github:RCTPushNotification", + "//xplat/js/react-native-github:ReactInternal", + ], +) + +rn_xplat_cxx_library2( + name = "RCTMapView_RNHeader", + header_namespace = "", + exported_headers = { + "React/RCTConvert+CoreLocation.h": RCTVIEWS_PATH + "RCTConvert+CoreLocation.h", + }, + labels = [ + "supermodule:xplat/default/public.react_native.infra", + ], + visibility = [ + "//fbobjc/Libraries/FBReactKit:RCTMapView", + "//fbobjc/VendorLib/react-native-maps:react-native-maps", + ], +) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 54af9968ff92a5..4944366f7b5ce3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -21,10 +21,19 @@ If you wish to contribute changes back to the **microsoft/react-native-macos** r git clone https://github.com/johndoe/react-native-macos.git ``` +### [Code of Conduct](https://github.com/facebook/react-native/blob/HEAD/CODE_OF_CONDUCT.md) + +As a reminder, all contributors are expected to adhere to the [Code of Conduct](https://github.com/facebook/react-native/blob/HEAD/CODE_OF_CONDUCT.md). + ### Setting up the upstream repository Before starting to contribute changes, please setup your upstream repository to the primary **microsoft/react-native-macos** repository. + +1. **Replying and handling open issues.** We get a lot of issues every day, and some of them may lack necessary information. You can help out by guiding people through the process of filling out the issue template, asking for clarifying information, or pointing them to existing issues that match their description of the problem. We cover more about this process in the [Issue Triage wiki](https://github.com/facebook/react-native/wiki/Triaging-GitHub-Issues). +2. **Reviewing pull requests for the docs.** Reviewing [documentation updates](https://github.com/facebook/react-native-website/pulls) can be as simple as checking for spelling and grammar. If you encounter situations that can be explained better in the docs, click **Edit** at the top of most docs pages to get started with your own contribution. +3. **Help people write test plans.** Some pull requests sent to the main repository may lack a proper test plan. These help reviewers understand how the change was tested, and can speed up the time it takes for a contribution to be accepted. + - When you run `git remote -v`, you should see only your fork in the output list ```bash @@ -42,6 +51,9 @@ git remote add upstream https://github.com/microsoft/react-native-macos.git - Now running `git remote -v` should show the upstream repository also +* [Issues](https://github.com/facebook/react-native/wiki/Triaging-GitHub-Issues) +* [Pull Requests](https://github.com/facebook/react-native/wiki/Managing-Pull-Requests) + ```bash git remote -v @@ -57,12 +69,19 @@ git remote -v For each bug or task you complete, it is recommended that you start with a fresh branch. If you have any lingering changes in your current branch that you want to save, go ahead and commit them. If you are just beginning, then you are good to go. On github, navigate to your repository which should be forked from **microsoft/react-native-macos** as described in the above sections. Above the list of files is a dropdown that should say master. Use the dropdown to create a new branch and name is according to what you will be working on. (I.e. DropdownHighlight, CleanUpExamples, etc). Now you have created a new branch. +* **React Native website** which contains the source code for the website, including the documentation, located at +* **Releases** are coordinated through the repository. This includes important documents such as the Changelog. +* **Discussions** about the future of React Native take place in the repository. +* **High-quality plugins** for React Native can be found throughout the [React Native Community GitHub Organization](http://github.com/react-native-community/). + **SourceTree:** If you are using SourceTree you will want your branch to show up in SourceTree so you can commit changes to your branch. It takes time for it to show up automatically, so you can make it show by running `git pull --all` in your command prompt from the root. Once you see your new branch in SourceTree under Remotes on the left navigation pane, double click on your branch to check it out locally. A dialog will come up and the default settings should be fine, click Ok. **Git Command Line** If you are using the command line, you will want to make sure you have your branch locally. It takes time for it to show up automatically, so you can make it show by running `git pull --all` in your command prompt from the root. Run `git branch -a` to see if your new branch shows up. Now you will want to check out your branch locally. You can do this with `git checkout -b branch-name`. Confirm you are now working out of the branch with `git branch`. +We use GitHub issues to track bugs exclusively. We have documented our issue handling processes in the [Issues wiki](https://github.com/facebook/react-native/wiki/Triaging-GitHub-Issues). + ### Merging upstream master into your fork master From time to time, your fork will get out of sync with the upstream remote. Use the following commands to get the master branch of your fork up up to date. @@ -102,6 +121,8 @@ sudo xcode-select -s /Applications/Xcode.app arch -x86_64 pod install ``` +The React Native blog is generated [from the Markdown sources for the blog](https://github.com/facebook/react-native-website/tree/HEAD/website/blog). + ### Make the fix Now that your branch is set up and ready for commits, go ahead and fix the bug you are working on or make some small change that you want to check in. @@ -114,6 +135,8 @@ yarn lint # run eslint on JavaScript yarn flow-check-macos # run Flow checks on JavaScript ``` +We recommend referring to the [CONTRIBUTING](https://github.com/facebook/react-native-website/blob/HEAD/CONTRIBUTING.md) document for the `react-native-website` repository to learn more about contributing to the website in general. + ### Commit your changes **SourceTree:** @@ -127,7 +150,18 @@ You can commit multiple times until you are ready to make a pull request. You sh ### Provide changelog information Run `yarn change` in the root of the repo. -### Create a Pull Request +1. Fork the React Native repository and create your branch from `main`. +2. Make the desired changes to React Native sources. Use the `packages/rn-tester` app to test them out. +3. If you've added code that should be tested, add tests. +4. If you've changed APIs, update the documentation, which lives in [another repo](https://github.com/facebook/react-native-website/). +5. Ensure the test suite passes, either locally or on CI once you opened a pull request. +6. Make sure your code lints (for example via `yarn lint --fix`). +7. Push the changes to your fork. +8. Create a pull request to the React Native repository. +9. Review and address comments on your pull request. + 1. A bot may comment with suggestions. Generally we ask you to resolve these first before a maintainer will review your code. + 2. If changes are requested and addressed, please [request review](https://docs.github.com/en/github/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/requesting-a-pull-request-review) to notify reviewers to take another look. +10. If you haven't already, please complete the [Contributor License Agreement](https://github.com/facebook/react-native/wiki/Contributor-License-Agreement) ("CLA"). **[Complete your CLA here.](https://code.facebook.com/cla)** **SourceTree:** In SourceTree click Push. @@ -135,13 +169,11 @@ In SourceTree click Push. **Git Command Line** Run `git push`. -This will push any staged files you have in your branch. - -Now go back to your fork on github. You should see a yellow bar at the top with your change and a button that says "Compare & Pull Request". Click that button. +Whenever you are ready to contribute code, check out our [step-by-step guide to sending your first pull request](https://github.com/facebook/react-native/wiki/How-to-Open-a-Pull-Request), or read the [How to Contribute Code](https://github.com/facebook/react-native/wiki/How-to-Contribute-Code) wiki for more details. Click "Create Pull Request". -A bunch of tests will automatically kick off to verify your PR. The tests marked as `required` must pass before a PR can be merged. +Tests help us prevent regressions from being introduced to the codebase. The GitHub repository is continuously tested using Circle and Appveyor, the results of which are available through the Checks functionality on [commits](https://github.com/facebook/react-native/commits/HEAD) and pull requests. You can learn more about running and writing tests in the [Tests wiki](http://github.com/facebook/react-native/wiki/Tests). Someone will also have to review your change before the change is allowed to be merged in. They may ask questions for more information or ask you to change things. Be sure to respond to their comments and push additional changes to the branch if they ask you to modify things before they sign off. diff --git a/Folly/folly/portability/Time.h b/Folly/folly/portability/Time.h index 0f024aab82f5d2..d4688ef8249900 100644 --- a/Folly/folly/portability/Time.h +++ b/Folly/folly/portability/Time.h @@ -28,8 +28,8 @@ // then enable our implementation on the source side so that // gets linked in instead. #if __MACH__ && \ - (MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_12 || \ - __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_10_0) + (MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_VERSION_11_0 || \ // TODO(macOS GH#774): Upgrade min allowed version to fix type redefinition bug with clock_t + __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_12_0) // TODO(macOS GH#774): Upgrade min allowed version to fix type redefinition bug with clock_t #ifdef FOLLY_HAVE_CLOCK_GETTIME #undef FOLLY_HAVE_CLOCK_GETTIME diff --git a/IntegrationTests/AccessibilityManagerTest.js b/IntegrationTests/AccessibilityManagerTest.js index 7501170f3acf89..a6f4c0d93bb64c 100644 --- a/IntegrationTests/AccessibilityManagerTest.js +++ b/IntegrationTests/AccessibilityManagerTest.js @@ -8,8 +8,6 @@ * @flow strict-local */ -'use strict'; - import invariant from 'invariant'; import NativeAccessibilityManager from 'react-native/Libraries/Components/AccessibilityInfo/NativeAccessibilityManager'; import {DeviceEventEmitter, NativeModules, View} from 'react-native'; diff --git a/IntegrationTests/AsyncStorageTest.js b/IntegrationTests/AsyncStorageTest.js index 12d9f3d620f331..51ce374556e90b 100644 --- a/IntegrationTests/AsyncStorageTest.js +++ b/IntegrationTests/AsyncStorageTest.js @@ -16,6 +16,7 @@ const {AsyncStorage, Text, View, StyleSheet} = ReactNative; const {TestModule} = ReactNative.NativeModules; const deepDiffer = require('react-native/Libraries/Utilities/differ/deepDiffer'); +const nullthrows = require('nullthrows'); const DEBUG = false; @@ -43,15 +44,32 @@ function expectTrue(condition: boolean, message: string) { } } +// Type-safe wrapper around JSON.stringify +function stringify( + value: + | void + | null + | string + | number + | boolean + | {...} + | $ReadOnlyArray, +): string { + if (typeof value === 'undefined') { + return 'undefined'; + } + return JSON.stringify(value); +} + function expectEqual(lhs, rhs, testname: string) { expectTrue( !deepDiffer(lhs, rhs), 'Error in test ' + testname + ': expected\n' + - JSON.stringify(rhs) + + stringify(rhs) + '\ngot\n' + - JSON.stringify(lhs), + stringify(lhs), ); } @@ -61,7 +79,7 @@ function expectAsyncNoError(place, err) { } expectTrue( err === null, - 'Unexpected error in ' + place + ': ' + JSON.stringify(err), + 'Unexpected error in ' + place + ': ' + stringify(err), ); } @@ -71,7 +89,7 @@ function testSetAndGet() { AsyncStorage.getItem(KEY_1, (err2, result) => { expectAsyncNoError('testSetAndGet/getItem', err2); expectEqual(result, VAL_1, 'testSetAndGet setItem'); - updateMessage('get(key_1) correctly returned ' + result); + updateMessage('get(key_1) correctly returned ' + String(result)); runTestCase('should get null for missing key', testMissingGet); }); }); @@ -81,7 +99,7 @@ function testMissingGet() { AsyncStorage.getItem(KEY_2, (err, result) => { expectAsyncNoError('testMissingGet/setItem', err); expectEqual(result, null, 'testMissingGet'); - updateMessage('missing get(key_2) correctly returned ' + result); + updateMessage('missing get(key_2) correctly returned ' + String(result)); runTestCase('check set twice results in a single key', testSetTwice); }); } @@ -105,8 +123,9 @@ function testRemoveItem() { AsyncStorage.getAllKeys((err, result) => { expectAsyncNoError('testRemoveItem/getAllKeys', err); expectTrue( - result.indexOf(KEY_1) >= 0 && result.indexOf(KEY_2) >= 0, - 'Missing KEY_1 or KEY_2 in ' + '(' + result + ')', + nullthrows(result).indexOf(KEY_1) >= 0 && + nullthrows(result).indexOf(KEY_2) >= 0, + 'Missing KEY_1 or KEY_2 in ' + '(' + nullthrows(result).join() + ')', ); updateMessage('testRemoveItem - add two items'); AsyncStorage.removeItem(KEY_1, err2 => { @@ -123,8 +142,8 @@ function testRemoveItem() { AsyncStorage.getAllKeys((err4, result3) => { expectAsyncNoError('testRemoveItem/getAllKeys', err4); expectTrue( - result3.indexOf(KEY_1) === -1, - 'Unexpected: KEY_1 present in ' + result3, + nullthrows(result3).indexOf(KEY_1) === -1, + 'Unexpected: KEY_1 present in ' + nullthrows(result3).join(), ); updateMessage('proper length returned.'); runTestCase('should merge values', testMerge); @@ -137,13 +156,17 @@ function testRemoveItem() { } function testMerge() { - AsyncStorage.setItem(KEY_MERGE, JSON.stringify(VAL_MERGE_1), err1 => { + AsyncStorage.setItem(KEY_MERGE, stringify(VAL_MERGE_1), err1 => { expectAsyncNoError('testMerge/setItem', err1); - AsyncStorage.mergeItem(KEY_MERGE, JSON.stringify(VAL_MERGE_2), err2 => { + AsyncStorage.mergeItem(KEY_MERGE, stringify(VAL_MERGE_2), err2 => { expectAsyncNoError('testMerge/mergeItem', err2); AsyncStorage.getItem(KEY_MERGE, (err3, result) => { expectAsyncNoError('testMerge/setItem', err3); - expectEqual(JSON.parse(result), VAL_MERGE_EXPECT, 'testMerge'); + expectEqual( + JSON.parse(nullthrows(result)), + VAL_MERGE_EXPECT, + 'testMerge', + ); updateMessage('objects deeply merged\nDone!'); runTestCase('multi set and get', testOptimizedMultiGet); }); @@ -165,8 +188,7 @@ function testOptimizedMultiGet() { expectAsyncNoError(`${i} testOptimizedMultiGet/multiGet`, err2); expectEqual(result, batch, `${i} testOptimizedMultiGet multiGet`); updateMessage( - 'multiGet([key_1, key_2]) correctly returned ' + - JSON.stringify(result), + 'multiGet([key_1, key_2]) correctly returned ' + stringify(result), ); done(); }); @@ -196,9 +218,10 @@ class AsyncStorageTest extends React.Component<{...}, $FlowFixMeState> { return ( - {/* $FlowFixMe(>=0.54.0 site=react_native_fb,react_native_oss) This - * comment suppresses an error found when Flow v0.54 was deployed. - * To see the error delete this comment and run Flow. */ + {/* $FlowFixMe[incompatible-type] (>=0.54.0 site=react_native_fb,react_ + * native_oss) This comment suppresses an error found when Flow v0.54 + * was deployed. To see the error delete this comment and run Flow. + */ this.constructor.displayName + ': '} {this.state.done ? 'Done' : 'Testing...'} {'\n\n' + this.state.messages} diff --git a/IntegrationTests/BUCK b/IntegrationTests/BUCK new file mode 100644 index 00000000000000..61e3de6466aaa5 --- /dev/null +++ b/IntegrationTests/BUCK @@ -0,0 +1,30 @@ +load("@fbsource//tools/build_defs:js_glob.bzl", "js_glob") +load("@fbsource//tools/build_defs/oss:metro_defs.bzl", "rn_library") + +# This file was generated by running +# js1 build buckfiles + +rn_library( + name = "IntegrationTests", + srcs = js_glob( + [ + "**/*", + ], + excludes = [ + "**/__*__/**", + "**/*.command", + "**/*.md", + "websocket_integration_test_server.js", + ], + ), + labels = ["supermodule:xplat/default/public.react_native.tests"], + skip_processors = True, + visibility = ["PUBLIC"], + deps = [ + "//xplat/js:node_modules__invariant", + "//xplat/js:node_modules__nullthrows", + "//xplat/js/RKJSModules/vendor/react:react", + "//xplat/js/react-native-github:react-native", + "//xplat/js/react-native-github/packages/assets:assets", + ], +) diff --git a/IntegrationTests/GlobalEvalWithSourceUrlTest.js b/IntegrationTests/GlobalEvalWithSourceUrlTest.js index fca7bfbc2170c7..a69ee3728c48b0 100644 --- a/IntegrationTests/GlobalEvalWithSourceUrlTest.js +++ b/IntegrationTests/GlobalEvalWithSourceUrlTest.js @@ -10,7 +10,7 @@ 'use strict'; -import type {ExtendedError} from 'react-native/Libraries/Core/Devtools/parseErrorStack'; +import type {ExtendedError} from 'react-native/Libraries/Core/ExtendedError'; const React = require('react'); const ReactNative = require('react-native'); diff --git a/IntegrationTests/ImageCachePolicyTest.js b/IntegrationTests/ImageCachePolicyTest.js index 03857091072cf6..00d65fb736d119 100644 --- a/IntegrationTests/ImageCachePolicyTest.js +++ b/IntegrationTests/ImageCachePolicyTest.js @@ -64,7 +64,7 @@ class ImageCachePolicyTest extends React.Component { { { { { return ( - {/* $FlowFixMe(>=0.54.0 site=react_native_fb,react_native_oss) This - * comment suppresses an error found when Flow v0.54 was deployed. - * To see the error delete this comment and run Flow. */ + {/* $FlowFixMe[incompatible-type] (>=0.54.0 site=react_native_fb,react_ + * native_oss) This comment suppresses an error found when Flow v0.54 + * was deployed. To see the error delete this comment and run Flow. + */ this.constructor.displayName + ': '} {this.state.done ? 'Done' : 'Testing...'} diff --git a/IntegrationTests/IntegrationTestsApp.js b/IntegrationTests/IntegrationTestsApp.js index 1e52877b7b9908..af41da151c47de 100644 --- a/IntegrationTests/IntegrationTestsApp.js +++ b/IntegrationTests/IntegrationTestsApp.js @@ -40,9 +40,9 @@ const TESTS = [ ]; TESTS.forEach( - /* $FlowFixMe(>=0.54.0 site=react_native_fb,react_native_oss) This comment - * suppresses an error found when Flow v0.54 was deployed. To see the error - * delete this comment and run Flow. */ + /* $FlowFixMe[incompatible-call] (>=0.54.0 site=react_native_fb,react_native_ + * oss) This comment suppresses an error found when Flow v0.54 was deployed. + * To see the error delete this comment and run Flow. */ test => AppRegistry.registerComponent(test.displayName, () => test), ); @@ -60,9 +60,10 @@ class IntegrationTestsApp extends React.Component<{...}, $FlowFixMeState> { if (this.state.test) { return ( - {/* $FlowFixMe(>=0.53.0 site=react_native_fb,react_native_oss) This - * comment suppresses an error when upgrading Flow's support for - * React. To see the error delete this comment and run Flow. */} + {/* $FlowFixMe[type-as-value] (>=0.53.0 site=react_native_fb,react_ + * native_oss) This comment suppresses an error when upgrading + * Flow's support for React. To see the error delete this comment + * and run Flow. */} ); @@ -79,9 +80,10 @@ class IntegrationTestsApp extends React.Component<{...}, $FlowFixMeState> { {TESTS.map(test => [ this.setState({test})} - /* $FlowFixMe(>=0.115.0 site=react_native_fb) This comment - * suppresses an error found when Flow v0.115 was deployed. To - * see the error, delete this comment and run Flow. */ + /* $FlowFixMe[incompatible-type] (>=0.115.0 site=react_native_fb) + * This comment suppresses an error found when Flow v0.115 was + * deployed. To see the error, delete this comment and run Flow. + */ style={styles.row}> {test.displayName} , diff --git a/IntegrationTests/LayoutEventsTest.js b/IntegrationTests/LayoutEventsTest.js index d0c784ca545021..3993a6dcca5e09 100644 --- a/IntegrationTests/LayoutEventsTest.js +++ b/IntegrationTests/LayoutEventsTest.js @@ -203,4 +203,5 @@ const styles = StyleSheet.create({ }, }); +LayoutEventsTest.displayName = 'LayoutEventsTest'; module.exports = LayoutEventsTest; diff --git a/IntegrationTests/ReactContentSizeUpdateTest.js b/IntegrationTests/ReactContentSizeUpdateTest.js index 7abcc4fb8b4627..9c826d9f4f5328 100644 --- a/IntegrationTests/ReactContentSizeUpdateTest.js +++ b/IntegrationTests/ReactContentSizeUpdateTest.js @@ -8,8 +8,6 @@ * @flow strict-local */ -'use strict'; - const RCTNativeAppEventEmitter = require('react-native/Libraries/EventEmitter/RCTNativeAppEventEmitter'); const React = require('react'); const ReactNative = require('react-native'); diff --git a/IntegrationTests/SizeFlexibilityUpdateTest.js b/IntegrationTests/SizeFlexibilityUpdateTest.js index b2e4952b102c70..3577a5363b5339 100644 --- a/IntegrationTests/SizeFlexibilityUpdateTest.js +++ b/IntegrationTests/SizeFlexibilityUpdateTest.js @@ -8,8 +8,6 @@ * @flow strict-local */ -'use strict'; - const RCTNativeAppEventEmitter = require('react-native/Libraries/EventEmitter/RCTNativeAppEventEmitter'); const React = require('react'); const ReactNative = require('react-native'); diff --git a/IntegrationTests/TimersTest.js b/IntegrationTests/TimersTest.js index 8550c6f00eec6b..30473e3b8e752c 100644 --- a/IntegrationTests/TimersTest.js +++ b/IntegrationTests/TimersTest.js @@ -97,36 +97,46 @@ class TimersTest extends React.Component { } componentDidMount() { + // $FlowFixMe[method-unbinding] added when improving typing for this parameters this.setTimeout(this.testSetTimeout0, 1000); } testSetTimeout0() { + // $FlowFixMe[method-unbinding] added when improving typing for this parameters this.setTimeout(this.testSetTimeout1, 0); } testSetTimeout1() { + // $FlowFixMe[method-unbinding] added when improving typing for this parameters this.setTimeout(this.testSetTimeout50, 1); } testSetTimeout50() { + // $FlowFixMe[method-unbinding] added when improving typing for this parameters this.setTimeout(this.testRequestAnimationFrame, 50); } testRequestAnimationFrame() { + // $FlowFixMe[method-unbinding] added when improving typing for this parameters this.requestAnimationFrame(this.testSetInterval0); } testSetInterval0() { + // $FlowFixMe[method-unbinding] added when improving typing for this parameters this._nextTest = this.testSetInterval20; + // $FlowFixMe[method-unbinding] added when improving typing for this parameters this._interval = this.setInterval(this._incrementInterval, 0); } testSetInterval20() { + // $FlowFixMe[method-unbinding] added when improving typing for this parameters this._nextTest = this.testSetImmediate; + // $FlowFixMe[method-unbinding] added when improving typing for this parameters this._interval = this.setInterval(this._incrementInterval, 20); } testSetImmediate() { + // $FlowFixMe[method-unbinding] added when improving typing for this parameters this.setImmediate(this.testClearTimeout0); } @@ -139,6 +149,7 @@ class TimersTest extends React.Component { testClearTimeout30() { const timeout = this.setTimeout(() => this._fail('testClearTimeout30'), 30); this.clearTimeout(timeout); + // $FlowFixMe[method-unbinding] added when improving typing for this parameters this.setTimeout(this.testClearMulti, 50); } @@ -156,6 +167,7 @@ class TimersTest extends React.Component { fails.forEach(timeout => this.clearTimeout(timeout)); this.setTimeout(() => this.clearTimeout(delayClear), 20); + // $FlowFixMe[method-unbinding] added when improving typing for this parameters this.setTimeout(this.testOrdering, 50); } @@ -191,6 +203,7 @@ class TimersTest extends React.Component { ), 25, ); + // $FlowFixMe[method-unbinding] added when improving typing for this parameters this.setTimeout(this.done, 50); } @@ -249,6 +262,7 @@ class TimersTest extends React.Component { this.clearInterval(this._interval); this._interval = null; } + // $FlowFixMe[method-unbinding] this.setState({count: 0}, this._nextTest); return; } @@ -267,4 +281,5 @@ const styles = StyleSheet.create({ }, }); +TimersTest.displayName = 'TimersTest'; module.exports = TimersTest; diff --git a/Libraries/ActionSheetIOS/ActionSheetIOS.js b/Libraries/ActionSheetIOS/ActionSheetIOS.js index bc3a935dee298b..786839498f334f 100644 --- a/Libraries/ActionSheetIOS/ActionSheetIOS.js +++ b/Libraries/ActionSheetIOS/ActionSheetIOS.js @@ -8,8 +8,6 @@ * @format */ -'use strict'; - import RCTActionSheetManager from './NativeActionSheetManager'; const invariant = require('invariant'); @@ -33,6 +31,7 @@ const ActionSheetIOS = { * - `destructiveButtonIndex` (int or array of ints) - index or indices of destructive buttons in `options` * - `title` (string) - a title to show above the action sheet * - `message` (string) - a message to show below the title + * - `disabledButtonIndices` (array of numbers) - a list of button indices which should be disabled * * The 'callback' function takes one parameter, the zero-based index * of the selected item. @@ -49,6 +48,7 @@ const ActionSheetIOS = { +anchor?: ?number, +tintColor?: ColorValue | ProcessedColorValue, +userInterfaceStyle?: string, + +disabledButtonIndices?: Array, |}, callback: (buttonIndex: number) => void, ) { diff --git a/Libraries/ActionSheetIOS/NativeActionSheetManager.js b/Libraries/ActionSheetIOS/NativeActionSheetManager.js index 063d9147e1d69b..7c48f8e2d4bbc3 100644 --- a/Libraries/ActionSheetIOS/NativeActionSheetManager.js +++ b/Libraries/ActionSheetIOS/NativeActionSheetManager.js @@ -8,8 +8,6 @@ * @format */ -'use strict'; - import type {TurboModule} from '../TurboModule/RCTExport'; import * as TurboModuleRegistry from '../TurboModule/TurboModuleRegistry'; @@ -25,6 +23,7 @@ export interface Spec extends TurboModule { +anchor?: ?number, +tintColor?: ?number, +userInterfaceStyle?: ?string, + +disabledButtonIndices?: Array, |}, callback: (buttonIndex: number) => void, ) => void; diff --git a/Libraries/ActionSheetIOS/React-RCTActionSheet.podspec b/Libraries/ActionSheetIOS/React-RCTActionSheet.podspec index c43897e0a3c877..c6554c3d7ec534 100644 --- a/Libraries/ActionSheetIOS/React-RCTActionSheet.podspec +++ b/Libraries/ActionSheetIOS/React-RCTActionSheet.podspec @@ -11,7 +11,7 @@ version = package['version'] source = { :git => 'https://github.com/facebook/react-native.git' } if version == '1000.0.0' # This is an unpublished version, use the latest commit hash of the react-native repo, which we’re presumably in. - source[:commit] = `git rev-parse HEAD`.strip + source[:commit] = `git rev-parse HEAD`.strip if system("git rev-parse --git-dir > /dev/null 2>&1") else source[:tag] = "v#{version}" end @@ -24,7 +24,7 @@ Pod::Spec.new do |s| s.documentation_url = "https://reactnative.dev/docs/actionsheetios" s.license = package["license"] s.author = "Facebook, Inc. and its affiliates" - s.platforms = { :ios => "10.0", :osx => "10.14" } # TODO(macOS GH#214) + s.platforms = { :ios => "11.0", :osx => "10.15" } # TODO(macOS GH#214) s.source = source s.source_files = "*.{m}" s.preserve_paths = "package.json", "LICENSE", "LICENSE-docs" diff --git a/Libraries/Alert/Alert.js b/Libraries/Alert/Alert.js index 17d306e9e985f3..cfa4cd80b9e983 100644 --- a/Libraries/Alert/Alert.js +++ b/Libraries/Alert/Alert.js @@ -8,13 +8,9 @@ * @flow */ -'use strict'; - import AlertMacOS from './AlertMacOS'; // TODO(macOS GH#774) import Platform from '../Utilities/Platform'; -import NativeDialogManagerAndroid, { - type DialogOptions, -} from '../NativeModules/specs/NativeDialogManagerAndroid'; +import type {DialogOptions} from '../NativeModules/specs/NativeDialogManagerAndroid'; import RCTAlertManager from './RCTAlertManager'; export type AlertType = @@ -54,6 +50,8 @@ class Alert { ) { Alert.prompt(title, message, buttons, 'default'); } else if (Platform.OS === 'android') { + const NativeDialogManagerAndroid = require('../NativeModules/specs/NativeDialogManagerAndroid') + .default; if (!NativeDialogManagerAndroid) { return; } diff --git a/Libraries/Alert/NativeAlertManager.js b/Libraries/Alert/NativeAlertManager.js index 929c165f32e584..c9f9327096bfd2 100644 --- a/Libraries/Alert/NativeAlertManager.js +++ b/Libraries/Alert/NativeAlertManager.js @@ -8,8 +8,6 @@ * @format */ -'use strict'; - import type {TurboModule} from '../TurboModule/RCTExport'; import * as TurboModuleRegistry from '../TurboModule/TurboModuleRegistry'; import type {DefaultInputsArray} from './AlertMacOS'; // TODO(macOS GH#774) diff --git a/Libraries/Alert/RCTAlertManager.android.js b/Libraries/Alert/RCTAlertManager.android.js index 43f49a94e50bb1..366692bd906d04 100644 --- a/Libraries/Alert/RCTAlertManager.android.js +++ b/Libraries/Alert/RCTAlertManager.android.js @@ -7,8 +7,6 @@ * @format */ -'use strict'; - import NativeDialogManagerAndroid from '../NativeModules/specs/NativeDialogManagerAndroid'; function emptyCallback() {} diff --git a/Libraries/Alert/RCTAlertManager.ios.js b/Libraries/Alert/RCTAlertManager.ios.js index 8ededddf4b463c..a5dbdac5daf7dc 100644 --- a/Libraries/Alert/RCTAlertManager.ios.js +++ b/Libraries/Alert/RCTAlertManager.ios.js @@ -8,8 +8,6 @@ * @flow strict-local */ -'use strict'; - import NativeAlertManager from './NativeAlertManager'; import type {Args} from './NativeAlertManager'; diff --git a/Libraries/Animated/Animated.js b/Libraries/Animated/Animated.js index 323f8d38afc3e9..2f6ee99ae6b04e 100644 --- a/Libraries/Animated/Animated.js +++ b/Libraries/Animated/Animated.js @@ -8,8 +8,6 @@ * @format */ -'use strict'; - import Platform from '../Utilities/Platform'; import typeof AnimatedFlatList from './components/AnimatedFlatList'; import typeof AnimatedImage from './components/AnimatedImage'; diff --git a/Libraries/Animated/AnimatedEvent.js b/Libraries/Animated/AnimatedEvent.js index 98ff42eb197071..b6de725948a961 100644 --- a/Libraries/Animated/AnimatedEvent.js +++ b/Libraries/Animated/AnimatedEvent.js @@ -74,6 +74,7 @@ function attachNativeEvent( NativeAnimatedHelper.API.removeAnimatedEventFromView( viewTag, eventName, + // $FlowFixMe[incompatible-call] mapping.animatedValueTag, ); }); @@ -210,9 +211,9 @@ class AnimatedEvent { } } else if (typeof recMapping === 'object') { for (const mappingKey in recMapping) { - /* $FlowFixMe(>=0.120.0) This comment suppresses an error found - * when Flow v0.120 was deployed. To see the error, delete this - * comment and run Flow. */ + /* $FlowFixMe[prop-missing] (>=0.120.0) This comment suppresses an + * error found when Flow v0.120 was deployed. To see the error, + * delete this comment and run Flow. */ traverse(recMapping[mappingKey], recEvt[mappingKey], mappingKey); } } diff --git a/Libraries/Animated/AnimatedImplementation.js b/Libraries/Animated/AnimatedImplementation.js index 12b225abab0616..706b87ea01e9e1 100644 --- a/Libraries/Animated/AnimatedImplementation.js +++ b/Libraries/Animated/AnimatedImplementation.js @@ -706,6 +706,4 @@ module.exports = { * Expose Event class, so it can be used as a type for type checkers. */ Event: AnimatedEvent, - - __PropsOnlyForTests: AnimatedProps, }; diff --git a/Libraries/Animated/AnimatedMock.js b/Libraries/Animated/AnimatedMock.js index eecfbb884ec42b..ca6741719482d9 100644 --- a/Libraries/Animated/AnimatedMock.js +++ b/Libraries/Animated/AnimatedMock.js @@ -14,7 +14,6 @@ const {AnimatedEvent, attachNativeEvent} = require('./AnimatedEvent'); const AnimatedImplementation = require('./AnimatedImplementation'); const AnimatedInterpolation = require('./nodes/AnimatedInterpolation'); const AnimatedNode = require('./nodes/AnimatedNode'); -const AnimatedProps = require('./nodes/AnimatedProps'); const AnimatedValue = require('./nodes/AnimatedValue'); const AnimatedValueXY = require('./nodes/AnimatedValueXY'); @@ -24,7 +23,6 @@ import type {EndCallback} from './animations/Animation'; import type {TimingAnimationConfig} from './animations/TimingAnimation'; import type {DecayAnimationConfig} from './animations/DecayAnimation'; import type {SpringAnimationConfig} from './animations/SpringAnimation'; -import type {Mapping, EventConfig} from './AnimatedEvent'; /** * Animations are a source of flakiness in snapshot testing. This mock replaces @@ -58,7 +56,12 @@ const spring = function( return { ...emptyAnimation, start: (callback?: ?EndCallback): void => { - anyValue.setValue(config.toValue); + // TODO(macOS GH#774) - setValue can't handle AnimatedNodes + if (config.toValue instanceof AnimatedNode) { + anyValue.setValue(config.toValue.__getValue()); + } else { + anyValue.setValue(config.toValue); + } callback && callback({finished: true}); }, }; @@ -123,10 +126,6 @@ const loop = function( return emptyAnimation; }; -const event = function(argMapping: Array, config: EventConfig): any { - return null; -}; - module.exports = { Value: AnimatedValue, ValueXY: AnimatedValueXY, @@ -146,11 +145,10 @@ module.exports = { parallel, stagger, loop, - event, + event: AnimatedImplementation.event, createAnimatedComponent, attachNativeEvent, forkEvent: AnimatedImplementation.forkEvent, unforkEvent: AnimatedImplementation.unforkEvent, Event: AnimatedEvent, - __PropsOnlyForTests: AnimatedProps, }; diff --git a/Libraries/Animated/AnimatedWeb.js b/Libraries/Animated/AnimatedWeb.js index 3f3313fe6c69e2..513f322b709d08 100644 --- a/Libraries/Animated/AnimatedWeb.js +++ b/Libraries/Animated/AnimatedWeb.js @@ -14,10 +14,13 @@ const AnimatedImplementation = require('./AnimatedImplementation'); module.exports = { ...AnimatedImplementation, - // $FlowFixMe createAnimatedComponent expects to receive types. Plain intrinsic components can't be typed like this + /* $FlowFixMe[incompatible-call] createAnimatedComponent expects to receive + * types. Plain intrinsic components can't be typed like this */ div: (AnimatedImplementation.createAnimatedComponent('div'): $FlowFixMe), - // $FlowFixMe createAnimatedComponent expects to receive types. Plain intrinsic components can't be typed like this + /* $FlowFixMe[incompatible-call] createAnimatedComponent expects to receive + * types. Plain intrinsic components can't be typed like this */ span: (AnimatedImplementation.createAnimatedComponent('span'): $FlowFixMe), - // $FlowFixMe createAnimatedComponent expects to receive types. Plain intrinsic components can't be typed like this + /* $FlowFixMe[incompatible-call] createAnimatedComponent expects to receive + * types. Plain intrinsic components can't be typed like this */ img: (AnimatedImplementation.createAnimatedComponent('img'): $FlowFixMe), }; diff --git a/Libraries/Animated/NativeAnimatedHelper.js b/Libraries/Animated/NativeAnimatedHelper.js index af78a8d0987591..536b44c1cbdba5 100644 --- a/Libraries/Animated/NativeAnimatedHelper.js +++ b/Libraries/Animated/NativeAnimatedHelper.js @@ -8,8 +8,6 @@ * @format */ -'use strict'; - import NativeAnimatedNonTurboModule from './NativeAnimatedModule'; import NativeAnimatedTurboModule from './NativeAnimatedTurboModule'; import NativeEventEmitter from '../EventEmitter/NativeEventEmitter'; @@ -49,9 +47,9 @@ const API = { saveValueCallback: (value: number) => void, ): void { invariant(NativeAnimatedModule, 'Native animated module is not available'); - if (NativeAnimatedModule.getValue) { + API.queueOperation(() => { NativeAnimatedModule.getValue(tag, saveValueCallback); - } + }); }, setWaitingForIdentifier: function(id: string): void { waitingForQueuedOperations.add(id); @@ -395,10 +393,17 @@ module.exports = { assertNativeAnimatedModule, shouldUseNativeDriver, transformDataType, - // $FlowExpectedError - unsafe getter lint suppresion + // $FlowExpectedError[unsafe-getters-setters] - unsafe getter lint suppresion + // $FlowExpectedError[missing-type-arg] - unsafe getter lint suppresion get nativeEventEmitter(): NativeEventEmitter { if (!nativeEventEmitter) { - nativeEventEmitter = new NativeEventEmitter(NativeAnimatedModule); + nativeEventEmitter = new NativeEventEmitter( + // T88715063: NativeEventEmitter only used this parameter on iOS. Now it uses it on all platforms, so this code was modified automatically to preserve its behavior + // If you want to use the native module on other platforms, please remove this condition and test its behavior + Platform.OS !== 'ios' && Platform.OS !== 'macos' // TODO(macOS GH#774): Also use this parameter on macOS + ? null + : NativeAnimatedModule, + ); } return nativeEventEmitter; }, diff --git a/Libraries/Animated/NativeAnimatedModule.js b/Libraries/Animated/NativeAnimatedModule.js index be8ceb140fc31c..d126bfdaff96eb 100644 --- a/Libraries/Animated/NativeAnimatedModule.js +++ b/Libraries/Animated/NativeAnimatedModule.js @@ -8,8 +8,6 @@ * @format */ -'use strict'; - import type {TurboModule} from '../TurboModule/RCTExport'; import * as TurboModuleRegistry from '../TurboModule/TurboModuleRegistry'; diff --git a/Libraries/Animated/NativeAnimatedTurboModule.js b/Libraries/Animated/NativeAnimatedTurboModule.js index b19fbde5f9b0f3..03ff0f26116560 100644 --- a/Libraries/Animated/NativeAnimatedTurboModule.js +++ b/Libraries/Animated/NativeAnimatedTurboModule.js @@ -8,8 +8,6 @@ * @format */ -'use strict'; - import type {TurboModule} from '../TurboModule/RCTExport'; import * as TurboModuleRegistry from '../TurboModule/TurboModuleRegistry'; diff --git a/Libraries/Animated/__tests__/Animated-test.js b/Libraries/Animated/__tests__/Animated-test.js index aa32306b7f5985..2b2ffe04dfdfb8 100644 --- a/Libraries/Animated/__tests__/Animated-test.js +++ b/Libraries/Animated/__tests__/Animated-test.js @@ -8,8 +8,7 @@ * @emails oncall+react_native */ -'use strict'; - +import AnimatedProps from '../nodes/AnimatedProps'; import TestRenderer from 'react-test-renderer'; import * as React from 'react'; @@ -23,6 +22,7 @@ jest.mock('../../BatchedBridge/NativeModules', () => ({ })); let Animated = require('../Animated'); + describe('Animated tests', () => { beforeEach(() => { jest.resetModules(); @@ -34,7 +34,7 @@ describe('Animated tests', () => { const callback = jest.fn(); - const node = new Animated.__PropsOnlyForTests( + const node = new AnimatedProps( { style: { backgroundColor: 'red', @@ -57,8 +57,6 @@ describe('Animated tests', () => { callback, ); - expect(anim.__getChildren().length).toBe(3); - expect(node.__getValue()).toEqual({ style: { backgroundColor: 'red', @@ -71,6 +69,12 @@ describe('Animated tests', () => { }, }); + expect(anim.__getChildren().length).toBe(0); + + node.__attach(); + + expect(anim.__getChildren().length).toBe(3); + anim.setValue(0.5); expect(callback).toBeCalled(); @@ -788,7 +792,7 @@ describe('Animated tests', () => { const callback = jest.fn(); - const node = new Animated.__PropsOnlyForTests( + const node = new AnimatedProps( { style: { opacity: vec.x.interpolate({ @@ -811,6 +815,10 @@ describe('Animated tests', () => { }, }); + node.__attach(); + + expect(callback.mock.calls.length).toBe(0); + vec.setValue({x: 42, y: 1492}); expect(callback.mock.calls.length).toBe(2); // once each for x, y @@ -892,7 +900,7 @@ describe('Animated tests', () => { const value3 = new Animated.Value(0); const value4 = Animated.add(value3, Animated.multiply(value1, value2)); const callback = jest.fn(); - const view = new Animated.__PropsOnlyForTests( + const view = new AnimatedProps( { style: { transform: [ @@ -904,6 +912,7 @@ describe('Animated tests', () => { }, callback, ); + view.__attach(); const listener = jest.fn(); const id = value4.addListener(listener); value3.setValue(137); diff --git a/Libraries/Animated/__tests__/AnimatedNative-test.js b/Libraries/Animated/__tests__/AnimatedNative-test.js index ceb93dccd332fe..e5ecec1fb7d648 100644 --- a/Libraries/Animated/__tests__/AnimatedNative-test.js +++ b/Libraries/Animated/__tests__/AnimatedNative-test.js @@ -8,8 +8,6 @@ * @emails oncall+react_native */ -'use strict'; - jest .clearAllMocks() .mock('../../BatchedBridge/NativeModules', () => ({ diff --git a/Libraries/Animated/__tests__/createAnimatedComponentInjection-test.js b/Libraries/Animated/__tests__/createAnimatedComponentInjection-test.js new file mode 100644 index 00000000000000..e82ea170a3eb95 --- /dev/null +++ b/Libraries/Animated/__tests__/createAnimatedComponentInjection-test.js @@ -0,0 +1,67 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @emails oncall+react_native + * @flow strict-local + * @format + */ + +'use strict'; + +const createAnimatedComponent = require('../createAnimatedComponent'); +const createAnimatedComponentInjection = require('../createAnimatedComponentInjection'); +const React = require('react'); + +function injected( + Component: React.AbstractComponent, +): React.AbstractComponent { + return createAnimatedComponent(Component); +} + +beforeEach(() => { + jest.resetModules(); + jest.resetAllMocks(); +}); + +test('does nothing without injection', () => { + expect(typeof createAnimatedComponent).toBe('function'); + expect(createAnimatedComponent).not.toBe(injected); +}); + +test('injection overrides `createAnimatedComponent`', () => { + createAnimatedComponentInjection.inject(injected); + + expect(createAnimatedComponent).toBe(injected); +}); + +test('injection errors if called too late', () => { + jest.spyOn(console, 'error').mockReturnValue(undefined); + + // Causes `createAnimatedComponent` to be initialized. + createAnimatedComponent; + + createAnimatedComponentInjection.inject(injected); + + expect(createAnimatedComponent).not.toBe(injected); + expect(console.error).toBeCalledWith( + 'createAnimatedComponentInjection: Must be called before `createAnimatedComponent`.', + ); +}); + +test('injection errors if called more than once', () => { + jest.spyOn(console, 'error').mockReturnValue(undefined); + + createAnimatedComponentInjection.inject(injected); + + expect(createAnimatedComponent).toBe(injected); + expect(console.error).not.toBeCalled(); + + createAnimatedComponentInjection.inject(injected); + + expect(console.error).toBeCalledWith( + 'createAnimatedComponentInjection: Cannot be called more than once.', + ); +}); diff --git a/Libraries/Animated/animations/Animation.js b/Libraries/Animated/animations/Animation.js index 53967d96f9e109..3555bf2cc2665a 100644 --- a/Libraries/Animated/animations/Animation.js +++ b/Libraries/Animated/animations/Animation.js @@ -71,6 +71,7 @@ class Animation { this.__nativeId, animatedValue.__getNativeTag(), this.__getNativeAnimationConfig(), + // $FlowFixMe[method-unbinding] added when improving typing for this parameters this.__debouncedOnEnd.bind(this), ); } catch (e) { diff --git a/Libraries/Animated/animations/DecayAnimation.js b/Libraries/Animated/animations/DecayAnimation.js index c3dce534f84baa..b04b911e7600c7 100644 --- a/Libraries/Animated/animations/DecayAnimation.js +++ b/Libraries/Animated/animations/DecayAnimation.js @@ -84,6 +84,7 @@ class DecayAnimation extends Animation { if (this._useNativeDriver) { this.__startNativeAnimation(animatedValue); } else { + // $FlowFixMe[method-unbinding] added when improving typing for this parameters this._animationFrame = requestAnimationFrame(this.onUpdate.bind(this)); } } @@ -105,6 +106,7 @@ class DecayAnimation extends Animation { this._lastValue = value; if (this.__active) { + // $FlowFixMe[method-unbinding] added when improving typing for this parameters this._animationFrame = requestAnimationFrame(this.onUpdate.bind(this)); } } diff --git a/Libraries/Animated/animations/SpringAnimation.js b/Libraries/Animated/animations/SpringAnimation.js index aec87e5cecb732..e09db93b7c5340 100644 --- a/Libraries/Animated/animations/SpringAnimation.js +++ b/Libraries/Animated/animations/SpringAnimation.js @@ -344,6 +344,7 @@ class SpringAnimation extends Animation { this.__debouncedOnEnd({finished: true}); return; } + // $FlowFixMe[method-unbinding] added when improving typing for this parameters this._animationFrame = requestAnimationFrame(this.onUpdate.bind(this)); } diff --git a/Libraries/Animated/animations/TimingAnimation.js b/Libraries/Animated/animations/TimingAnimation.js index ffe8c8313be2f7..7f239ce0102faf 100644 --- a/Libraries/Animated/animations/TimingAnimation.js +++ b/Libraries/Animated/animations/TimingAnimation.js @@ -48,6 +48,7 @@ let _easeInOut; function easeInOut() { if (!_easeInOut) { const Easing = require('../Easing'); + // $FlowFixMe[method-unbinding] _easeInOut = Easing.inOut(Easing.ease); } return _easeInOut; @@ -117,6 +118,7 @@ class TimingAnimation extends Animation { this.__startNativeAnimation(animatedValue); } else { this._animationFrame = requestAnimationFrame( + // $FlowFixMe[method-unbinding] added when improving typing for this parameters this.onUpdate.bind(this), ); } @@ -149,6 +151,7 @@ class TimingAnimation extends Animation { (this._toValue - this._fromValue), ); if (this.__active) { + // $FlowFixMe[method-unbinding] added when improving typing for this parameters this._animationFrame = requestAnimationFrame(this.onUpdate.bind(this)); } } diff --git a/Libraries/Animated/components/AnimatedFlatList.js b/Libraries/Animated/components/AnimatedFlatList.js index 64ba6c200beea0..49a1ebcbd19b37 100644 --- a/Libraries/Animated/components/AnimatedFlatList.js +++ b/Libraries/Animated/components/AnimatedFlatList.js @@ -8,8 +8,6 @@ * @format */ -'use strict'; - import * as React from 'react'; const FlatList = require('../../Lists/FlatList'); diff --git a/Libraries/Animated/components/AnimatedImage.js b/Libraries/Animated/components/AnimatedImage.js index 56cdb7d18f7908..107d601f50c91e 100644 --- a/Libraries/Animated/components/AnimatedImage.js +++ b/Libraries/Animated/components/AnimatedImage.js @@ -8,8 +8,6 @@ * @format */ -'use strict'; - import * as React from 'react'; const Image = require('../../Image/Image'); @@ -17,9 +15,9 @@ const createAnimatedComponent = require('../createAnimatedComponent'); import type {AnimatedComponentType} from '../createAnimatedComponent'; -module.exports = (createAnimatedComponent( - (Image: $FlowFixMe), -): AnimatedComponentType< +module.exports = (createAnimatedComponent((Image: $FlowFixMe), { + collapsable: false, +}): AnimatedComponentType< React.ElementConfig, React.ElementRef, >); diff --git a/Libraries/Animated/components/AnimatedScrollView.js b/Libraries/Animated/components/AnimatedScrollView.js index 259cbb81b0ee2f..488e9d12041534 100644 --- a/Libraries/Animated/components/AnimatedScrollView.js +++ b/Libraries/Animated/components/AnimatedScrollView.js @@ -8,8 +8,6 @@ * @format */ -'use strict'; - import * as React from 'react'; const ScrollView = require('../../Components/ScrollView/ScrollView'); @@ -24,9 +22,9 @@ const ScrollViewWithEventThrottle = React.forwardRef((props, ref) => ( )); -module.exports = (createAnimatedComponent( - ScrollViewWithEventThrottle, -): AnimatedComponentType< +module.exports = (createAnimatedComponent(ScrollViewWithEventThrottle, { + collapsable: false, +}): AnimatedComponentType< React.ElementConfig, React.ElementRef, >); diff --git a/Libraries/Animated/components/AnimatedSectionList.js b/Libraries/Animated/components/AnimatedSectionList.js index 9beef55e879488..98ccd918460bc6 100644 --- a/Libraries/Animated/components/AnimatedSectionList.js +++ b/Libraries/Animated/components/AnimatedSectionList.js @@ -8,11 +8,9 @@ * @format */ -'use strict'; - import * as React from 'react'; -const SectionList = require('../../Lists/SectionList'); +import SectionList from '../../Lists/SectionList'; const createAnimatedComponent = require('../createAnimatedComponent'); import type {AnimatedComponentType} from '../createAnimatedComponent'; diff --git a/Libraries/Animated/components/AnimatedText.js b/Libraries/Animated/components/AnimatedText.js index 5a184e0626fe17..dd4fb0b02378fb 100644 --- a/Libraries/Animated/components/AnimatedText.js +++ b/Libraries/Animated/components/AnimatedText.js @@ -8,8 +8,6 @@ * @format */ -'use strict'; - import * as React from 'react'; const Text = require('../../Text/Text'); @@ -17,9 +15,9 @@ const createAnimatedComponent = require('../createAnimatedComponent'); import type {AnimatedComponentType} from '../createAnimatedComponent'; -module.exports = (createAnimatedComponent( - (Text: $FlowFixMe), -): AnimatedComponentType< +module.exports = (createAnimatedComponent((Text: $FlowFixMe), { + collapsable: false, +}): AnimatedComponentType< React.ElementConfig, React.ElementRef, >); diff --git a/Libraries/Animated/components/AnimatedView.js b/Libraries/Animated/components/AnimatedView.js index 0ce54601fe9990..79ebe1a0ea97cb 100644 --- a/Libraries/Animated/components/AnimatedView.js +++ b/Libraries/Animated/components/AnimatedView.js @@ -8,8 +8,6 @@ * @format */ -'use strict'; - import * as React from 'react'; const View = require('../../Components/View/View'); @@ -17,7 +15,9 @@ const createAnimatedComponent = require('../createAnimatedComponent'); import type {AnimatedComponentType} from '../createAnimatedComponent'; -module.exports = (createAnimatedComponent(View): AnimatedComponentType< +module.exports = (createAnimatedComponent(View, { + collapsable: true, +}): AnimatedComponentType< React.ElementConfig, React.ElementRef, >); diff --git a/Libraries/Animated/createAnimatedComponent.js b/Libraries/Animated/createAnimatedComponent.js index 230aa164203606..1154557c407908 100644 --- a/Libraries/Animated/createAnimatedComponent.js +++ b/Libraries/Animated/createAnimatedComponent.js @@ -10,6 +10,8 @@ 'use strict'; +import * as createAnimatedComponentInjection from './createAnimatedComponentInjection'; + const View = require('../Components/View/View'); const {AnimatedEvent} = require('./AnimatedEvent'); const AnimatedProps = require('./nodes/AnimatedProps'); @@ -37,8 +39,13 @@ export type AnimatedComponentType< Instance, >; +type AnimatedComponentOptions = { + collapsable?: boolean, +}; + function createAnimatedComponent( Component: React.AbstractComponent, + options?: AnimatedComponentOptions, ): AnimatedComponentType { invariant( typeof Component !== 'function' || @@ -79,6 +86,11 @@ function createAnimatedComponent( } _isFabric = (): boolean => { + // When called during the first render, `_component` is always null. + // Therefore, even if a component is rendered in Fabric, we can't detect + // that until ref is set, which happens sometime after the first render. + // In cases where this value switching between "false" and "true" on Fabric + // causes issues, add an additional check for _component nullity. if (this._component == null) { return false; } @@ -164,14 +176,11 @@ function createAnimatedComponent( _attachProps(nextProps) { const oldPropsAnimated = this._propsAnimated; - if (nextProps === oldPropsAnimated) { - return; - } - this._propsAnimated = new AnimatedProps( nextProps, this._animatedPropsCallback, ); + this._propsAnimated.__attach(); // When you call detach, it removes the element from the parent list // of children. If it goes to 0, then the parent also detaches itself @@ -192,19 +201,6 @@ function createAnimatedComponent( setLocalRef: ref => { this._prevComponent = this._component; this._component = ref; - - // TODO: Delete this in a future release. - if (ref != null && ref.getNode == null) { - ref.getNode = () => { - console.warn( - '%s: Calling `getNode()` on the ref of an Animated component ' + - 'is no longer necessary. You can now directly use the ref ' + - 'instead. This method will be removed in a future release.', - ref.constructor.name ?? '<>', - ); - return ref; - }; - } }, }); @@ -213,23 +209,42 @@ function createAnimatedComponent( const {style: passthruStyle = {}, ...passthruProps} = this.props.passthroughAnimatedPropExplicitValues || {}; const mergedStyle = {...style, ...passthruStyle}; + + // On Fabric, we always want to ensure the container Animated View is *not* + // flattened. + // Because we do not get a host component ref immediately and thus cannot + // do a proper Fabric vs non-Fabric detection immediately, we default to assuming + // that Fabric *is* enabled until we know otherwise. + // Thus, in Fabric, this view will never be flattened. In non-Fabric, the view will + // not be flattened during the initial render but may be flattened in the second render + // and onwards. + const forceNativeIdFabric = + (this._component == null && + (options?.collapsable === false || props.collapsable !== true)) || + this._isFabric(); + + const forceNativeId = + props.collapsable ?? + (this._propsAnimated.__isNative || + forceNativeIdFabric || + options?.collapsable === false); + // The native driver updates views directly through the UI thread so we + // have to make sure the view doesn't get optimized away because it cannot + // go through the NativeViewHierarchyManager since it operates on the shadow + // thread. TODO: T68258846 + const collapsableProps = forceNativeId + ? { + nativeID: props.nativeID ?? 'animatedComponent', + collapsable: false, + } + : {}; return ( ); } @@ -270,6 +285,8 @@ function createAnimatedComponent( this._propsAnimated && this._propsAnimated.__detach(); this._detachNativeEvents(); this._markUpdateComplete(); + this._component = null; + this._prevComponent = null; } } @@ -283,4 +300,6 @@ function createAnimatedComponent( }); } -module.exports = createAnimatedComponent; +// $FlowIgnore[incompatible-cast] - Will be compatible after refactors. +module.exports = (createAnimatedComponentInjection.recordAndRetrieve() ?? + createAnimatedComponent: typeof createAnimatedComponent); diff --git a/Libraries/Animated/createAnimatedComponentInjection.js b/Libraries/Animated/createAnimatedComponentInjection.js new file mode 100644 index 00000000000000..5a1727e959a9b7 --- /dev/null +++ b/Libraries/Animated/createAnimatedComponentInjection.js @@ -0,0 +1,48 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + */ + +import * as React from 'react'; + +type createAnimatedComponent = ( + Component: React.AbstractComponent, +) => React.AbstractComponent; + +// This can be undefined, null, or the experimental implementation. If this is +// null, that means `createAnimatedComponent` has already been initialized and +// it is too late to call `inject`. +let injected: ?createAnimatedComponent; + +/** + * Call during bundle initialization to opt-in to new `createAnimatedComponent`. + */ +export function inject(newInjected: createAnimatedComponent): void { + if (injected !== undefined) { + if (__DEV__) { + console.error( + 'createAnimatedComponentInjection: ' + + (injected == null + ? 'Must be called before `createAnimatedComponent`.' + : 'Cannot be called more than once.'), + ); + } + return; + } + injected = newInjected; +} + +/** + * Only called by `createAnimatedComponent.js`. + */ +export function recordAndRetrieve(): createAnimatedComponent | null { + if (injected === undefined) { + injected = null; + } + return injected; +} diff --git a/Libraries/Animated/createAnimatedComponent_EXPERIMENTAL.js b/Libraries/Animated/createAnimatedComponent_EXPERIMENTAL.js new file mode 100644 index 00000000000000..a7d5f8169e5636 --- /dev/null +++ b/Libraries/Animated/createAnimatedComponent_EXPERIMENTAL.js @@ -0,0 +1,48 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + */ + +import useAnimatedProps from './useAnimatedProps'; +import useMergeRefs from '../Utilities/useMergeRefs'; +import StyleSheet from '../StyleSheet/StyleSheet'; +import * as React from 'react'; + +/** + * Experimental implementation of `createAnimatedComponent` that is intended to + * be compatible with concurrent rendering. + */ +export default function createAnimatedComponent( + Component: React.AbstractComponent, +): React.AbstractComponent { + return React.forwardRef((props, forwardedRef) => { + const [reducedProps, callbackRef] = useAnimatedProps( + props, + ); + const ref = useMergeRefs(callbackRef, forwardedRef); + + // Some components require explicit passthrough values for animation + // to work properly. For example, if an animated component is + // transformed and Pressable, onPress will not work after transform + // without these passthrough values. + // $FlowFixMe[prop-missing] + const {passthroughAnimatedPropExplicitValues, style} = reducedProps; + const {style: passthroughStyle, ...passthroughProps} = + passthroughAnimatedPropExplicitValues ?? {}; + const mergedStyle = StyleSheet.compose(style, passthroughStyle); + + return ( + + ); + }); +} diff --git a/Libraries/Animated/nodes/AnimatedInterpolation.js b/Libraries/Animated/nodes/AnimatedInterpolation.js index 63dfa670f1705e..ce06272b5c6d79 100644 --- a/Libraries/Animated/nodes/AnimatedInterpolation.js +++ b/Libraries/Animated/nodes/AnimatedInterpolation.js @@ -205,14 +205,12 @@ function createInterpolationFromStringOutputRange( // [200, 250], // [0, 0.5], // ] - /* $FlowFixMe(>=0.18.0): `outputRange[0].match()` can return `null`. Need to - * guard against this possibility. - */ + /* $FlowFixMe[incompatible-use] (>=0.18.0): `outputRange[0].match()` can + * return `null`. Need to guard against this possibility. */ const outputRanges = outputRange[0].match(stringShapeRegex).map(() => []); outputRange.forEach(value => { - /* $FlowFixMe(>=0.18.0): `value.match()` can return `null`. Need to guard - * against this possibility. - */ + /* $FlowFixMe[incompatible-use] (>=0.18.0): `value.match()` can return + * `null`. Need to guard against this possibility. */ value.match(stringShapeRegex).forEach((number, i) => { outputRanges[i].push(+number); }); @@ -220,8 +218,10 @@ function createInterpolationFromStringOutputRange( const interpolations = outputRange[0] .match(stringShapeRegex) - /* $FlowFixMe(>=0.18.0): `outputRange[0].match()` can return `null`. Need - * to guard against this possibility. */ + /* $FlowFixMe[incompatible-use] (>=0.18.0): `outputRange[0].match()` can + * return `null`. Need to guard against this possibility. */ + /* $FlowFixMe[incompatible-call] (>=0.18.0): `outputRange[0].match()` can + * return `null`. Need to guard against this possibility. */ .map((value, i) => { return createInterpolation({ ...config, @@ -277,12 +277,11 @@ function checkValidInputRange(arr: $ReadOnlyArray) { for (let i = 1; i < arr.length; ++i) { invariant( arr[i] >= arr[i - 1], - /* $FlowFixMe(>=0.13.0) - In the addition expression below this comment, - * one or both of the operands may be something that doesn't cleanly - * convert to a string, like undefined, null, and object, etc. If you really - * mean this implicit string conversion, you can do something like - * String(myThing) - */ + /* $FlowFixMe[incompatible-type] (>=0.13.0) - In the addition expression + * below this comment, one or both of the operands may be something that + * doesn't cleanly convert to a string, like undefined, null, and object, + * etc. If you really mean this implicit string conversion, you can do + * something like String(myThing) */ 'inputRange must be monotonically non-decreasing ' + arr, ); } @@ -292,12 +291,11 @@ function checkInfiniteRange(name: string, arr: $ReadOnlyArray) { invariant(arr.length >= 2, name + ' must have at least 2 elements'); invariant( arr.length !== 2 || arr[0] !== -Infinity || arr[1] !== Infinity, - /* $FlowFixMe(>=0.13.0) - In the addition expression below this comment, - * one or both of the operands may be something that doesn't cleanly convert - * to a string, like undefined, null, and object, etc. If you really mean - * this implicit string conversion, you can do something like - * String(myThing) - */ + /* $FlowFixMe[incompatible-type] (>=0.13.0) - In the addition expression + * below this comment, one or both of the operands may be something that + * doesn't cleanly convert to a string, like undefined, null, and object, + * etc. If you really mean this implicit string conversion, you can do + * something like String(myThing) */ name + 'cannot be ]-infinity;+infinity[ ' + arr, ); } @@ -358,8 +356,9 @@ class AnimatedInterpolation extends AnimatedWithChildren { return { inputRange: this._config.inputRange, // Only the `outputRange` can contain strings so we don't need to transform `inputRange` here - /* $FlowFixMe(>=0.38.0) - Flow error detected during the deployment of - * v0.38.0. To see the error, remove this comment and run flow */ + /* $FlowFixMe[incompatible-call] (>=0.38.0) - Flow error detected during + * the deployment of v0.38.0. To see the error, remove this comment and + * run flow */ outputRange: this.__transformDataType(this._config.outputRange), extrapolateLeft: this._config.extrapolateLeft || this._config.extrapolate || 'extend', diff --git a/Libraries/Animated/nodes/AnimatedProps.js b/Libraries/Animated/nodes/AnimatedProps.js index 578ed6718d213f..6f915bb5b8fd1d 100644 --- a/Libraries/Animated/nodes/AnimatedProps.js +++ b/Libraries/Animated/nodes/AnimatedProps.js @@ -33,7 +33,6 @@ class AnimatedProps extends AnimatedNode { } this._props = props; this._callback = callback; - this.__attach(); } __getValue(): Object { diff --git a/Libraries/Animated/nodes/AnimatedValue.js b/Libraries/Animated/nodes/AnimatedValue.js index 1be77112eef6f5..92baefb2cc8e07 100644 --- a/Libraries/Animated/nodes/AnimatedValue.js +++ b/Libraries/Animated/nodes/AnimatedValue.js @@ -46,9 +46,9 @@ const NativeAnimatedAPI = NativeAnimatedHelper.API; function _flush(rootNode: AnimatedValue): void { const animatedStyles = new Set(); function findAnimatedStyles(node) { - /* $FlowFixMe(>=0.68.0 site=react_native_fb) This comment suppresses an - * error found when Flow v0.68 was deployed. To see the error delete this - * comment and run Flow. */ + /* $FlowFixMe[prop-missing] (>=0.68.0 site=react_native_fb) This comment + * suppresses an error found when Flow v0.68 was deployed. To see the error + * delete this comment and run Flow. */ if (typeof node.update === 'function') { animatedStyles.add(node); } else { @@ -56,10 +56,21 @@ function _flush(rootNode: AnimatedValue): void { } } findAnimatedStyles(rootNode); - /* $FlowFixMe */ + // $FlowFixMe[prop-missing] animatedStyles.forEach(animatedStyle => animatedStyle.update()); } +/** + * Some operations are executed only on batch end, which is _mostly_ scheduled when + * Animated component props change. For some of the changes which require immediate execution + * (e.g. setValue), we create a separate batch in case none is scheduled. + */ +function _executeAsAnimatedBatch(id: string, operation: () => void) { + NativeAnimatedAPI.setWaitingForIdentifier(id); + operation(); + NativeAnimatedAPI.unsetWaitingForIdentifier(id); +} + /** * Standard value for driving animations. One `Animated.Value` can drive * multiple properties in a synchronized fashion, but can only be driven by one @@ -115,7 +126,9 @@ class AnimatedValue extends AnimatedWithChildren { !this.__isNative /* don't perform a flush for natively driven values */, ); if (this.__isNative) { - NativeAnimatedAPI.setAnimatedNodeValue(this.__getNativeTag(), value); + _executeAsAnimatedBatch(this.__getNativeTag().toString(), () => { + NativeAnimatedAPI.setAnimatedNodeValue(this.__getNativeTag(), value); + }); } } diff --git a/Libraries/Animated/nodes/AnimatedWithChildren.js b/Libraries/Animated/nodes/AnimatedWithChildren.js index e46dbf2f629762..5f4ff1c6de1f91 100644 --- a/Libraries/Animated/nodes/AnimatedWithChildren.js +++ b/Libraries/Animated/nodes/AnimatedWithChildren.js @@ -76,6 +76,7 @@ class AnimatedWithChildren extends AnimatedNode { super.__callListeners(value); if (!this.__isNative) { for (const child of this._children) { + // $FlowFixMe[method-unbinding] added when improving typing for this parameters if (child.__getValue) { child.__callListeners(child.__getValue()); } diff --git a/Libraries/Animated/useAnimatedProps.js b/Libraries/Animated/useAnimatedProps.js new file mode 100644 index 00000000000000..142faedb1b2396 --- /dev/null +++ b/Libraries/Animated/useAnimatedProps.js @@ -0,0 +1,211 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + */ + +'use strict'; + +import AnimatedProps from './nodes/AnimatedProps'; +import {AnimatedEvent} from './AnimatedEvent'; +import useRefEffect from '../Utilities/useRefEffect'; +import NativeAnimatedHelper from './NativeAnimatedHelper'; +import { + useCallback, + useEffect, + useLayoutEffect, + useMemo, + useReducer, + useRef, + useState, +} from 'react'; + +type ReducedProps = { + ...TProps, + collapsable: boolean, + ... +}; +type CallbackRef = T => mixed; + +let animatedComponentNextId = 1; + +export default function useAnimatedProps( + props: TProps, +): [ReducedProps, CallbackRef] { + const [, scheduleUpdate] = useReducer(count => count + 1, 0); + const onUpdateRef = useRef void>(null); + + // TODO: Only invalidate `node` if animated props or `style` change. In the + // previous implementation, we permitted `style` to override props with the + // same name property name as styles, so we can probably continue doing that. + // The ordering of other props *should* not matter. + const node = useMemo( + () => new AnimatedProps(props, () => onUpdateRef.current?.()), + [props], + ); + useAnimatedPropsLifecycle(node); + + // TODO: This "effect" does three things: + // + // 1) Call `setNativeView`. + // 2) Update `onUpdateRef`. + // 3) Update listeners for `AnimatedEvent` props. + // + // Ideally, each of these would be separat "effects" so that they are not + // unnecessarily re-run when irrelevant dependencies change. For example, we + // should be able to hoist all `AnimatedEvent` props and only do #3 if either + // the `AnimatedEvent` props change or `instance` changes. + // + // But there is no way to transparently compose three separate callback refs, + // so we just combine them all into one for now. + const refEffect = useCallback( + instance => { + // NOTE: This may be called more often than necessary (e.g. when `props` + // changes), but `setNativeView` already optimizes for that. + node.setNativeView(instance); + + // NOTE: This callback is only used by the JavaScript animation driver. + onUpdateRef.current = () => { + if ( + process.env.NODE_ENV === 'test' || + typeof instance !== 'object' || + typeof instance?.setNativeProps !== 'function' || + isFabricInstance(instance) + ) { + // Schedule an update for this component to update `reducedProps`, + // but do not compute it immediately. If a parent also updated, we + // need to merge those new props in before updating. + scheduleUpdate(); + } else if (!node.__isNative) { + // $FlowIgnore[not-a-function] - Assume it's still a function. + instance.setNativeProps(node.__getAnimatedValue()); + } else { + throw new Error( + 'Attempting to run JS driven animation on animated node ' + + 'that has been moved to "native" earlier by starting an ' + + 'animation with `useNativeDriver: true`', + ); + } + }; + + const target = getEventTarget(instance); + const events = []; + + for (const propName in props) { + const propValue = props[propName]; + if (propValue instanceof AnimatedEvent && propValue.__isNative) { + propValue.__attach(target, propName); + events.push([propName, propValue]); + } + } + + return () => { + onUpdateRef.current = null; + + for (const [propName, propValue] of events) { + propValue.__detach(target, propName); + } + }; + }, + [props, node], + ); + const callbackRef = useRefEffect(refEffect); + + return [reduceAnimatedProps(node), callbackRef]; +} + +function reduceAnimatedProps( + node: AnimatedProps, +): ReducedProps { + // Force `collapsable` to be false so that the native view is not flattened. + // Flattened views cannot be accurately referenced by the native driver. + return { + ...node.__getValue(), + collapsable: false, + }; +} + +/** + * Manages the lifecycle of the supplied `AnimatedProps` by invoking `__attach` + * and `__detach`. However, this is more complicated because `AnimatedProps` + * uses reference counting to determine when to recursively detach its children + * nodes. So in order to optimize this, we avoid detaching until the next attach + * unless we are unmounting. + */ +function useAnimatedPropsLifecycle(node: AnimatedProps): void { + const prevNodeRef = useRef(null); + const isUnmountingRef = useRef(false); + + const [animatedComponentId] = useState( + () => `${animatedComponentNextId++}:animatedComponent`, + ); + + useLayoutEffect(() => { + NativeAnimatedHelper.API.setWaitingForIdentifier(animatedComponentId); + }); + + useEffect(() => { + NativeAnimatedHelper.API.unsetWaitingForIdentifier(animatedComponentId); + }); + + useLayoutEffect(() => { + isUnmountingRef.current = false; + return () => { + isUnmountingRef.current = true; + }; + }, []); + + useLayoutEffect(() => { + node.__attach(); + if (prevNodeRef.current != null) { + const prevNode = prevNodeRef.current; + // TODO: Stop restoring default values (unless `reset` is called). + prevNode.__restoreDefaultValues(); + prevNode.__detach(); + prevNodeRef.current = null; + } + return () => { + if (isUnmountingRef.current) { + // NOTE: Do not restore default values on unmount, see D18197735. + node.__detach(); + } else { + prevNodeRef.current = node; + } + }; + }, [node]); +} + +function getEventTarget(instance: TInstance): TInstance { + return typeof instance === 'object' && + typeof instance?.getScrollableNode === 'function' + ? // $FlowFixMe[incompatible-use] - Legacy instance assumptions. + instance.getScrollableNode() + : instance; +} + +// $FlowFixMe[unclear-type] - Legacy instance assumptions. +function isFabricInstance(instance: any): boolean { + return ( + hasFabricHandle(instance) || + // Some components have a setNativeProps function but aren't a host component + // such as lists like FlatList and SectionList. These should also use + // forceUpdate in Fabric since setNativeProps doesn't exist on the underlying + // host component. This crazy hack is essentially special casing those lists and + // ScrollView itself to use forceUpdate in Fabric. + // If these components end up using forwardRef then these hacks can go away + // as instance would actually be the underlying host component and the above check + // would be sufficient. + hasFabricHandle(instance?.getNativeScrollRef?.()) || + hasFabricHandle(instance?.getScrollResponder?.()?.getNativeScrollRef?.()) + ); +} + +// $FlowFixMe[unclear-type] - Legacy instance assumptions. +function hasFabricHandle(instance: any): boolean { + // eslint-disable-next-line dot-notation + return instance?.['_internalInstanceHandle']?.stateNode?.canonical != null; +} diff --git a/Libraries/AppState/AppState.js b/Libraries/AppState/AppState.js index cc8a62a381ff8e..04492206127d79 100644 --- a/Libraries/AppState/AppState.js +++ b/Libraries/AppState/AppState.js @@ -4,17 +4,30 @@ * 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 - * @flow */ -'use strict'; - +import {type EventSubscription} from '../vendor/emitter/EventEmitter'; import NativeEventEmitter from '../EventEmitter/NativeEventEmitter'; import logError from '../Utilities/logError'; -import EventEmitter from '../vendor/emitter/EventEmitter'; import NativeAppState from './NativeAppState'; -import invariant from 'invariant'; +import Platform from '../Utilities/Platform'; + +export type AppStateValues = 'inactive' | 'background' | 'active'; + +type AppStateEventDefinitions = { + change: [AppStateValues], + memoryWarning: [], + blur: [], + focus: [], +}; + +type NativeAppStateEventDefinitions = { + appStateDidChange: [{app_state: AppStateValues}], + appStateFocusChange: [boolean], + memoryWarning: [], +}; /** * `AppState` can tell you if the app is in the foreground or background, @@ -22,44 +35,52 @@ import invariant from 'invariant'; * * See https://reactnative.dev/docs/appstate.html */ -class AppState extends NativeEventEmitter { - _eventHandlers: Object; - _supportedEvents = ['change', 'memoryWarning', 'blur', 'focus']; - currentState: ?string; +class AppState { + currentState: ?string = null; isAvailable: boolean; - constructor() { - super(NativeAppState); - - this.isAvailable = true; - this._eventHandlers = this._supportedEvents.reduce((handlers, key) => { - handlers[key] = new Map(); - return handlers; - }, {}); - - this.currentState = NativeAppState.getConstants().initialAppState; - - let eventUpdated = false; + _emitter: ?NativeEventEmitter; - // TODO: this is a terrible solution - in order to ensure `currentState` - // prop is up to date, we have to register an observer that updates it - // whenever the state changes, even if nobody cares. We should just - // deprecate the `currentState` property and get rid of this. - this.addListener('appStateDidChange', appStateData => { - eventUpdated = true; - this.currentState = appStateData.app_state; - }); - - // TODO: see above - this request just populates the value of `currentState` - // when the module is first initialized. Would be better to get rid of the - // prop and expose `getCurrentAppState` method directly. - NativeAppState.getCurrentAppState(appStateData => { - // It's possible that the state will have changed here & listeners need to be notified - if (!eventUpdated && this.currentState !== appStateData.app_state) { + constructor() { + if (NativeAppState == null) { + this.isAvailable = false; + } else { + this.isAvailable = true; + + const emitter: NativeEventEmitter = new NativeEventEmitter( + // T88715063: NativeEventEmitter only used this parameter on iOS. Now it uses it on all platforms, so this code was modified automatically to preserve its behavior + // If you want to use the native module on other platforms, please remove this condition and test its behavior + Platform.OS !== 'ios' && Platform.OS !== 'macos' // TODO(macOS GH#774): Also use this parameter on macOS + ? null + : NativeAppState, + ); + this._emitter = emitter; + + this.currentState = NativeAppState.getConstants().initialAppState; + + let eventUpdated = false; + + // TODO: this is a terrible solution - in order to ensure `currentState` + // prop is up to date, we have to register an observer that updates it + // whenever the state changes, even if nobody cares. We should just + // deprecate the `currentState` property and get rid of this. + emitter.addListener('appStateDidChange', appStateData => { + eventUpdated = true; this.currentState = appStateData.app_state; - this.emit('appStateDidChange', appStateData); - } - }, logError); + }); + + // TODO: see above - this request just populates the value of `currentState` + // when the module is first initialized. Would be better to get rid of the + // prop and expose `getCurrentAppState` method directly. + // $FlowExpectedError[incompatible-call] + NativeAppState.getCurrentAppState(appStateData => { + // It's possible that the state will have changed here & listeners need to be notified + if (!eventUpdated && this.currentState !== appStateData.app_state) { + this.currentState = appStateData.app_state; + emitter.emit('appStateDidChange', appStateData); + } + }, logError); + } } // TODO: now that AppState is a subclass of NativeEventEmitter, we could @@ -73,107 +94,72 @@ class AppState extends NativeEventEmitter { * * See https://reactnative.dev/docs/appstate.html#addeventlistener */ - addEventListener(type: string, handler: Function) { - invariant( - this._supportedEvents.indexOf(type) !== -1, - 'Trying to subscribe to unknown event: "%s"', - type, - ); - + addEventListener>( + type: K, + handler: (...$ElementType) => void, + ): EventSubscription { + const emitter = this._emitter; + if (emitter == null) { + throw new Error('Cannot use AppState when `isAvailable` is false.'); + } switch (type) { - case 'change': { - this._eventHandlers[type].set( - handler, - this.addListener('appStateDidChange', appStateData => { - handler(appStateData.app_state); - }), - ); - break; - } - case 'memoryWarning': { - this._eventHandlers[type].set( - handler, - this.addListener('memoryWarning', handler), - ); - break; - } - + case 'change': + // $FlowIssue[invalid-tuple-arity] Flow cannot refine handler based on the event type + const changeHandler: AppStateValues => void = handler; + return emitter.addListener('appStateDidChange', appStateData => { + changeHandler(appStateData.app_state); + }); + case 'memoryWarning': + // $FlowIssue[invalid-tuple-arity] Flow cannot refine handler based on the event type + const memoryWarningHandler: () => void = handler; + return emitter.addListener('memoryWarning', memoryWarningHandler); case 'blur': - case 'focus': { - this._eventHandlers[type].set( - handler, - this.addListener('appStateFocusChange', hasFocus => { - if (type === 'blur' && !hasFocus) { - handler(); - } - if (type === 'focus' && hasFocus) { - handler(); - } - }), - ); - } + case 'focus': + // $FlowIssue[invalid-tuple-arity] Flow cannot refine handler based on the event type + const focusOrBlurHandler: () => void = handler; + return emitter.addListener('appStateFocusChange', hasFocus => { + if (type === 'blur' && !hasFocus) { + focusOrBlurHandler(); + } + if (type === 'focus' && hasFocus) { + focusOrBlurHandler(); + } + }); } + throw new Error('Trying to subscribe to unknown event: ' + type); } /** - * Remove a handler by passing the `change` event type and the handler. - * - * See https://reactnative.dev/docs/appstate.html#removeeventlistener + * @deprecated Use `remove` on the EventSubscription from `addEventListener`. */ - removeEventListener(type: string, handler: Function) { - invariant( - this._supportedEvents.indexOf(type) !== -1, - 'Trying to remove listener for unknown event: "%s"', - type, - ); - if (!this._eventHandlers[type].has(handler)) { - return; + removeEventListener>( + type: K, + listener: (...$ElementType) => mixed, + ): void { + const emitter = this._emitter; + if (emitter == null) { + throw new Error('Cannot use AppState when `isAvailable` is false.'); } - this._eventHandlers[type].get(handler).remove(); - this._eventHandlers[type].delete(handler); - } -} - -function throwMissingNativeModule() { - invariant( - false, - 'Cannot use AppState module when native RCTAppState is not included in the build.\n' + - 'Either include it, or check AppState.isAvailable before calling any methods.', - ); -} - -class MissingNativeAppStateShim extends EventEmitter { - // AppState - isAvailable: boolean = false; - currentState: ?string = null; - - addEventListener(type: string, handler: Function) { - throwMissingNativeModule(); - } - - removeEventListener(type: string, handler: Function) { - throwMissingNativeModule(); - } - - // EventEmitter - addListener() { - throwMissingNativeModule(); - } - - removeAllListeners() { - throwMissingNativeModule(); - } - - removeSubscription() { - throwMissingNativeModule(); + // NOTE: This will report a deprecation notice via `console.error`. + switch (type) { + case 'change': + // $FlowIssue[invalid-tuple-arity] Flow cannot refine handler based on the event type + // $FlowIssue[incompatible-call] + emitter.removeListener('appStateDidChange', listener); + return; + case 'memoryWarning': + // $FlowIssue[invalid-tuple-arity] Flow cannot refine handler based on the event type + emitter.removeListener('memoryWarning', listener); + return; + case 'blur': + case 'focus': + // $FlowIssue[invalid-tuple-arity] Flow cannot refine handler based on the event type + // $FlowIssue[incompatible-call] + emitter.addListener('appStateFocusChange', listener); + return; + } + throw new Error('Trying to unsubscribe from unknown event: ' + type); } } -// This module depends on the native `RCTAppState` module. If you don't include it, -// `AppState.isAvailable` will return `false`, and any method calls will throw. -// We reassign the class variable to keep the autodoc generator happy. -const AppStateInstance: AppState | MissingNativeAppStateShim = NativeAppState - ? new AppState() - : new MissingNativeAppStateShim(); - -module.exports = AppStateInstance; +module.exports = (new AppState(): AppState); diff --git a/Libraries/AppState/NativeAppState.js b/Libraries/AppState/NativeAppState.js index 76f3b71619ef2c..b08fe5ca81a5c4 100644 --- a/Libraries/AppState/NativeAppState.js +++ b/Libraries/AppState/NativeAppState.js @@ -8,8 +8,6 @@ * @format */ -'use strict'; - import type {TurboModule} from '../TurboModule/RCTExport'; import * as TurboModuleRegistry from '../TurboModule/TurboModuleRegistry'; diff --git a/Libraries/BUCK b/Libraries/BUCK deleted file mode 100644 index 0993fce3acf8fe..00000000000000 --- a/Libraries/BUCK +++ /dev/null @@ -1,36 +0,0 @@ -# Copyright (c) Facebook, Inc. and its affiliates. -# -# This source code is licensed under the MIT license found in the -# LICENSE file in the root directory of this source tree. - -load("//tools/build_defs:fb_native_wrapper.bzl", "fb_native") -load( - "//tools/build_defs/oss:rn_codegen_defs.bzl", - "rn_codegen_modules", -) -load( - "//tools/build_defs/oss:rn_defs.bzl", - "react_native_root_target", -) - -fb_native.genrule( - name = "react_native_codegen_schema", - srcs = glob( - [ - "**/*.js", - ], - exclude = [ - "**/__tests__/**/*", - ], - ), - cmd = "$(exe {}) $OUT $SRCS".format(react_native_root_target("packages/react-native-codegen:write_to_json")), - out = "schema.json", - labels = ["codegen_rule"], -) - -rn_codegen_modules( - name = "FBReactNativeSpec", - library_labels = ["supermodule:xplat/default/public.react_native.infra"], - native_module_spec_name = "FBReactNativeSpec", - schema_target = ":react_native_codegen_schema", -) diff --git a/Libraries/BatchedBridge/MessageQueue.js b/Libraries/BatchedBridge/MessageQueue.js index fa2147b540d299..c01bbb14749ccb 100644 --- a/Libraries/BatchedBridge/MessageQueue.js +++ b/Libraries/BatchedBridge/MessageQueue.js @@ -47,7 +47,7 @@ class MessageQueue { _callID: number; _lastFlush: number; _eventLoopStartTime: number; - _immediatesCallback: ?() => void; + _reactNativeMicrotasksCallback: ?() => void; _debugInfo: {[number]: [number, number], ...}; _remoteModuleTable: {[number]: string, ...}; @@ -63,7 +63,7 @@ class MessageQueue { this._callID = 0; this._lastFlush = 0; this._eventLoopStartTime = Date.now(); - this._immediatesCallback = null; + this._reactNativeMicrotasksCallback = null; if (__DEV__) { this._debugInfo = {}; @@ -72,13 +72,16 @@ class MessageQueue { } // $FlowFixMe[cannot-write] + // $FlowFixMe[method-unbinding] added when improving typing for this parameters this.callFunctionReturnFlushedQueue = this.callFunctionReturnFlushedQueue.bind( this, ); // $FlowFixMe[cannot-write] + // $FlowFixMe[method-unbinding] added when improving typing for this parameters this.flushedQueue = this.flushedQueue.bind(this); // $FlowFixMe[cannot-write] + // $FlowFixMe[method-unbinding] added when improving typing for this parameters this.invokeCallbackAndReturnFlushedQueue = this.invokeCallbackAndReturnFlushedQueue.bind( this, ); @@ -129,7 +132,7 @@ class MessageQueue { flushedQueue(): null | [Array, Array, Array, number] { this.__guard(() => { - this.__callImmediates(); + this.__callReactNativeMicrotasks(); }); const queue = this._queue; @@ -145,14 +148,16 @@ class MessageQueue { this._lazyCallableModules[name] = () => module; } - registerLazyCallableModule(name: string, factory: void => {...}) { - let module: {...}; - let getValue: ?(void) => {...} = factory; + registerLazyCallableModule(name: string, factory: void => interface {}) { + let module: interface {}; + let getValue: ?(void) => interface {} = factory; this._lazyCallableModules[name] = () => { if (getValue) { module = getValue(); getValue = null; } + /* $FlowFixMe[class-object-subtyping] added when improving typing for + * this parameters */ return module; }; } @@ -349,8 +354,8 @@ class MessageQueue { // For JSTimers to register its callback. Otherwise a circular dependency // between modules is introduced. Note that only one callback may be // registered at a time. - setImmediatesCallback(fn: () => void) { - this._immediatesCallback = fn; + setReactNativeMicrotasksCallback(fn: () => void) { + this._reactNativeMicrotasksCallback = fn; } /** @@ -376,16 +381,16 @@ class MessageQueue { // can be configured by the VM or any Inspector __shouldPauseOnThrow(): boolean { return ( - // $FlowFixMe + // $FlowFixMe[cannot-resolve-name] typeof DebuggerInternal !== 'undefined' && DebuggerInternal.shouldPauseOnThrow === true // eslint-disable-line no-undef ); } - __callImmediates() { - Systrace.beginEvent('JSTimers.callImmediates()'); - if (this._immediatesCallback != null) { - this._immediatesCallback(); + __callReactNativeMicrotasks() { + Systrace.beginEvent('JSTimers.callReactNativeMicrotasks()'); + if (this._reactNativeMicrotasksCallback != null) { + this._reactNativeMicrotasksCallback(); } Systrace.endEvent(); } @@ -404,7 +409,7 @@ class MessageQueue { const moduleMethods = this.getCallableModule(module); invariant( !!moduleMethods, - `Module ${module} is not a registered callable module (calling ${method}). A frequent cause of the error is that the application entry file path is incorrect. + `Module ${module} is not a registered callable module (calling ${method}). A frequent cause of the error is that the application entry file path is incorrect. This can also happen when the JS bundle is corrupt or there is an early initialization error when loading React Native.`, ); invariant( diff --git a/Libraries/BatchedBridge/NativeModules.js b/Libraries/BatchedBridge/NativeModules.js index 4f764463738d9f..257436199d3484 100644 --- a/Libraries/BatchedBridge/NativeModules.js +++ b/Libraries/BatchedBridge/NativeModules.js @@ -14,7 +14,7 @@ const BatchedBridge = require('./BatchedBridge'); const invariant = require('invariant'); -import type {ExtendedError} from '../Core/Devtools/parseErrorStack'; +import type {ExtendedError} from '../Core/ExtendedError'; export type ModuleConfig = [ string /* name */, @@ -101,6 +101,8 @@ function genMethod(moduleID: number, methodID: number, type: MethodType) { if (type === 'promise') { fn = function promiseMethodWrapper(...args: Array) { // In case we reject, capture a useful stack trace here. + /* $FlowFixMe[class-object-subtyping] added when improving typing for + * this parameters */ const enqueueingFrameError: ExtendedError = new Error(); return new Promise((resolve, reject) => { BatchedBridge.enqueueNativeCall( @@ -166,6 +168,8 @@ function updateErrorWithErrorData( errorData: {message: string, ...}, error: ExtendedError, ): ExtendedError { + /* $FlowFixMe[class-object-subtyping] added when improving typing for this + * parameters */ return Object.assign(error, errorData || {}); } diff --git a/Libraries/BatchedBridge/__tests__/MessageQueue-test.js b/Libraries/BatchedBridge/__tests__/MessageQueue-test.js index e1b7fd84d32411..9c63238d8a099a 100644 --- a/Libraries/BatchedBridge/__tests__/MessageQueue-test.js +++ b/Libraries/BatchedBridge/__tests__/MessageQueue-test.js @@ -108,7 +108,7 @@ describe('MessageQueue', function() { const unknownModule = 'UnknownModule', unknownMethod = 'UnknownMethod'; expect(() => queue.__callFunction(unknownModule, unknownMethod)).toThrow( - `Module ${unknownModule} is not a registered callable module (calling ${unknownMethod}). A frequent cause of the error is that the application entry file path is incorrect. + `Module ${unknownModule} is not a registered callable module (calling ${unknownMethod}). A frequent cause of the error is that the application entry file path is incorrect. This can also happen when the JS bundle is corrupt or there is an early initialization error when loading React Native.`, ); }); diff --git a/Libraries/Blob/BlobManager.js b/Libraries/Blob/BlobManager.js index e12512392a3e78..4f4310adddb582 100644 --- a/Libraries/Blob/BlobManager.js +++ b/Libraries/Blob/BlobManager.js @@ -8,8 +8,6 @@ * @format */ -'use strict'; - const Blob = require('./Blob'); const BlobRegistry = require('./BlobRegistry'); diff --git a/Libraries/Blob/FileReader.js b/Libraries/Blob/FileReader.js index 15a8b44d0bb522..a71d10fc562d3d 100644 --- a/Libraries/Blob/FileReader.js +++ b/Libraries/Blob/FileReader.js @@ -8,8 +8,6 @@ * @format */ -'use strict'; - const Blob = require('./Blob'); const EventTarget = require('event-target-shim'); @@ -48,7 +46,7 @@ class FileReader extends (EventTarget(...READER_EVENTS): any) { _error: ?Error; _result: ?ReaderResult; _aborted: boolean = false; - _subscriptions: Array<*> = []; + _subscriptions: Array = []; constructor() { super(); diff --git a/Libraries/Blob/NativeBlobModule.js b/Libraries/Blob/NativeBlobModule.js index d58aeeab6ebd4f..d43c8bc3e3b21f 100644 --- a/Libraries/Blob/NativeBlobModule.js +++ b/Libraries/Blob/NativeBlobModule.js @@ -8,8 +8,6 @@ * @format */ -'use strict'; - import type {TurboModule} from '../TurboModule/RCTExport'; import * as TurboModuleRegistry from '../TurboModule/TurboModuleRegistry'; diff --git a/Libraries/Blob/NativeFileReaderModule.js b/Libraries/Blob/NativeFileReaderModule.js index ac135db7394d19..a33d5d9b7754dd 100644 --- a/Libraries/Blob/NativeFileReaderModule.js +++ b/Libraries/Blob/NativeFileReaderModule.js @@ -8,8 +8,6 @@ * @format */ -'use strict'; - import type {TurboModule} from '../TurboModule/RCTExport'; import * as TurboModuleRegistry from '../TurboModule/TurboModuleRegistry'; diff --git a/Libraries/Blob/RCTBlobManager.h b/Libraries/Blob/RCTBlobManager.h index 96c8f0722f02c9..04b905ccb92a24 100755 --- a/Libraries/Blob/RCTBlobManager.h +++ b/Libraries/Blob/RCTBlobManager.h @@ -8,8 +8,9 @@ #import #import #import +#import -@interface RCTBlobManager : NSObject +@interface RCTBlobManager : NSObject - (NSString *)store:(NSData *)data; diff --git a/Libraries/Blob/RCTBlobManager.mm b/Libraries/Blob/RCTBlobManager.mm index a75844ed6ae1ed..425dc73fb6c077 100755 --- a/Libraries/Blob/RCTBlobManager.mm +++ b/Libraries/Blob/RCTBlobManager.mm @@ -37,13 +37,11 @@ @implementation RCTBlobManager RCT_EXPORT_MODULE(BlobModule) @synthesize bridge = _bridge; +@synthesize moduleRegistry = _moduleRegistry; @synthesize methodQueue = _methodQueue; -@synthesize turboModuleRegistry = _turboModuleRegistry; -- (void)setBridge:(RCTBridge *)bridge +- (void)initialize { - _bridge = bridge; - std::lock_guard lock(_blobsMutex); _blobs = [NSMutableDictionary new]; @@ -140,9 +138,9 @@ - (void)remove:(NSString *)blobId RCT_EXPORT_METHOD(addNetworkingHandler) { - RCTNetworking *const networking = _bridge ? _bridge.networking : [_turboModuleRegistry moduleForName:"RCTNetworking"]; + RCTNetworking *const networking = [_moduleRegistry moduleForName:"Networking"]; - // TODO(T63516227): Why can methodQueue be nil here? + // TODO(T63516227): Why can methodQueue be nil here? // We don't want to do anything when methodQueue is nil. if (!networking.methodQueue) { return; @@ -156,23 +154,23 @@ - (void)remove:(NSString *)blobId RCT_EXPORT_METHOD(addWebSocketHandler:(double)socketID) { - dispatch_async(_bridge.webSocketModule.methodQueue, ^{ - [self->_bridge.webSocketModule setContentHandler:self forSocketID:[NSNumber numberWithDouble:socketID]]; + dispatch_async(((RCTWebSocketModule *)[_moduleRegistry moduleForName:"WebSocketModule"]).methodQueue, ^{ + [[self->_moduleRegistry moduleForName:"WebSocketModule"] setContentHandler:self forSocketID:[NSNumber numberWithDouble:socketID]]; }); } RCT_EXPORT_METHOD(removeWebSocketHandler:(double)socketID) { - dispatch_async(_bridge.webSocketModule.methodQueue, ^{ - [self->_bridge.webSocketModule setContentHandler:nil forSocketID:[NSNumber numberWithDouble:socketID]]; + dispatch_async(((RCTWebSocketModule *)[_moduleRegistry moduleForName:"WebSocketModule"]).methodQueue, ^{ + [[self->_moduleRegistry moduleForName:"WebSocketModule"] setContentHandler:nil forSocketID:[NSNumber numberWithDouble:socketID]]; }); } // @lint-ignore FBOBJCUNTYPEDCOLLECTION1 RCT_EXPORT_METHOD(sendOverSocket:(NSDictionary *)blob socketID:(double)socketID) { - dispatch_async(_bridge.webSocketModule.methodQueue, ^{ - [self->_bridge.webSocketModule sendData:[self resolve:blob] forSocketID:[NSNumber numberWithDouble:socketID]]; + dispatch_async(((RCTWebSocketModule *)[_moduleRegistry moduleForName:"WebSocketModule"]).methodQueue, ^{ + [[self->_moduleRegistry moduleForName:"WebSocketModule"] sendData:[self resolve:blob] forSocketID:[NSNumber numberWithDouble:socketID]]; }); } diff --git a/Libraries/Blob/RCTBlobPlugins.mm b/Libraries/Blob/RCTBlobPlugins.mm index d06f40e5d21120..289094f35d43a0 100644 --- a/Libraries/Blob/RCTBlobPlugins.mm +++ b/Libraries/Blob/RCTBlobPlugins.mm @@ -17,13 +17,14 @@ #import Class RCTBlobClassProvider(const char *name) { - static std::unordered_map sCoreModuleClassMap = { + // Intentionally leak to avoid crashing after static destructors are run. + static const auto sCoreModuleClassMap = new const std::unordered_map{ {"FileReaderModule", RCTFileReaderModuleCls}, {"BlobModule", RCTBlobManagerCls}, }; - auto p = sCoreModuleClassMap.find(name); - if (p != sCoreModuleClassMap.end()) { + auto p = sCoreModuleClassMap->find(name); + if (p != sCoreModuleClassMap->end()) { auto classFunc = p->second; return classFunc(); } diff --git a/Libraries/Blob/RCTFileReaderModule.mm b/Libraries/Blob/RCTFileReaderModule.mm index 315605acc1174d..2525de0c201b2b 100644 --- a/Libraries/Blob/RCTFileReaderModule.mm +++ b/Libraries/Blob/RCTFileReaderModule.mm @@ -23,15 +23,14 @@ @implementation RCTFileReaderModule RCT_EXPORT_MODULE(FileReaderModule) -@synthesize bridge = _bridge; -@synthesize turboModuleRegistry = _turboModuleRegistry; +@synthesize moduleRegistry = _moduleRegistry; RCT_EXPORT_METHOD(readAsText:(NSDictionary *)blob encoding:(NSString *)encoding resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) { - RCTBlobManager *blobManager = [[self bridge] moduleForClass:[RCTBlobManager class]]; + RCTBlobManager *blobManager = [_moduleRegistry moduleForName:"BlobModule"]; NSData *data = [blobManager resolve:blob]; if (data == nil) { @@ -56,12 +55,7 @@ @implementation RCTFileReaderModule resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) { - RCTBlobManager *blobManager = nil; - if ([self bridge]) { - blobManager = [[self bridge] moduleForClass:[RCTBlobManager class]]; - } else { - blobManager = [[self turboModuleRegistry] moduleForName:[NSStringFromClass([RCTBlobManager class]) UTF8String]]; - } + RCTBlobManager *blobManager = [_moduleRegistry moduleForName:"BlobModule"]; NSData *data = [blobManager resolve:blob]; if (data == nil) { diff --git a/Libraries/Blob/React-RCTBlob.podspec b/Libraries/Blob/React-RCTBlob.podspec index 8522ca699905cb..7edd6d980a7d66 100644 --- a/Libraries/Blob/React-RCTBlob.podspec +++ b/Libraries/Blob/React-RCTBlob.podspec @@ -11,13 +11,13 @@ version = package['version'] source = { :git => 'https://github.com/facebook/react-native.git' } if version == '1000.0.0' # This is an unpublished version, use the latest commit hash of the react-native repo, which we’re presumably in. - source[:commit] = `git rev-parse HEAD`.strip + source[:commit] = `git rev-parse HEAD`.strip if system("git rev-parse --git-dir > /dev/null 2>&1") else source[:tag] = "v#{version}" end folly_compiler_flags = '-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -Wno-comma -Wno-shorten-64-to-32' -folly_version = '2020.01.13.00' +folly_version = '2021.06.28.00-v2' Pod::Spec.new do |s| s.name = "React-RCTBlob" @@ -26,7 +26,7 @@ Pod::Spec.new do |s| s.homepage = "https://reactnative.dev/" s.license = package["license"] s.author = "Facebook, Inc. and its affiliates" - s.platforms = { :ios => "10.0", :osx => "10.14" } # TODO(macOS GH#214) + s.platforms = { :ios => "11.0", :osx => "10.15" } # TODO(macOS GH#214) s.compiler_flags = folly_compiler_flags + ' -Wno-nullability-completeness' s.source = source s.source_files = "*.{h,m,mm}" diff --git a/Libraries/Blob/URL.js b/Libraries/Blob/URL.js index bf9ab01bf1a786..3e63ff753fa179 100644 --- a/Libraries/Blob/URL.js +++ b/Libraries/Blob/URL.js @@ -7,8 +7,6 @@ * @format */ -'use strict'; - const Blob = require('./Blob'); import NativeBlobModule from './NativeBlobModule'; @@ -51,7 +49,7 @@ if ( * ``` */ -// Small subset from whatwg-url: https://github.com/jsdom/whatwg-url/tree/master/lib +// Small subset from whatwg-url: https://github.com/jsdom/whatwg-url/tree/master/src // The reference code bloat comes from Unicode issues with URLs, so those won't work here. export class URLSearchParams { _searchParams = []; @@ -67,27 +65,27 @@ export class URLSearchParams { } delete(name) { - throw new Error('not implemented'); + throw new Error('URLSearchParams.delete is not implemented'); } get(name) { - throw new Error('not implemented'); + throw new Error('URLSearchParams.get is not implemented'); } getAll(name) { - throw new Error('not implemented'); + throw new Error('URLSearchParams.getAll is not implemented'); } has(name) { - throw new Error('not implemented'); + throw new Error('URLSearchParams.has is not implemented'); } set(name, value) { - throw new Error('not implemented'); + throw new Error('URLSearchParams.set is not implemented'); } sort() { - throw new Error('not implemented'); + throw new Error('URLSearchParams.sort is not implemented'); } [Symbol.iterator]() { @@ -107,7 +105,7 @@ export class URLSearchParams { function validateBaseUrl(url: string) { // from this MIT-licensed gist: https://gist.github.com/dperini/729294 - return /^(?:(?:(?:https?|ftp):)?\/\/)(?:(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,}))?)(?::\d{2,5})?(?:[/?#]\S*)?$/i.test( + return /^(?:(?:(?:https?|ftp):)?\/\/)(?:(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z0-9\u00a1-\uffff][a-z0-9\u00a1-\uffff_-]{0,62})?[a-z0-9\u00a1-\uffff]\.)*(?:[a-z\u00a1-\uffff]{2,}\.?))(?::\d{2,5})?(?:[/?#]\S*)?$/.test( url, ); } @@ -156,15 +154,15 @@ export class URL { } get hash() { - throw new Error('not implemented'); + throw new Error('URL.hash is not implemented'); } get host() { - throw new Error('not implemented'); + throw new Error('URL.host is not implemented'); } get hostname() { - throw new Error('not implemented'); + throw new Error('URL.hostname is not implemented'); } get href(): string { @@ -172,27 +170,27 @@ export class URL { } get origin() { - throw new Error('not implemented'); + throw new Error('URL.origin is not implemented'); } get password() { - throw new Error('not implemented'); + throw new Error('URL.password is not implemented'); } get pathname() { - throw new Error('not implemented'); + throw new Error('URL.pathname not implemented'); } get port() { - throw new Error('not implemented'); + throw new Error('URL.port is not implemented'); } get protocol() { - throw new Error('not implemented'); + throw new Error('URL.protocol is not implemented'); } get search() { - throw new Error('not implemented'); + throw new Error('URL.search is not implemented'); } get searchParams(): URLSearchParams { @@ -215,6 +213,6 @@ export class URL { } get username() { - throw new Error('not implemented'); + throw new Error('URL.username is not implemented'); } } diff --git a/Libraries/BugReporting/BugReporting.js b/Libraries/BugReporting/BugReporting.js index d2f3c4a442a4c9..d0f8dc2b18c73e 100644 --- a/Libraries/BugReporting/BugReporting.js +++ b/Libraries/BugReporting/BugReporting.js @@ -8,8 +8,6 @@ * @flow strict-local */ -'use strict'; - import RCTDeviceEventEmitter from '../EventEmitter/RCTDeviceEventEmitter'; import NativeRedBox from '../NativeModules/specs/NativeRedBox'; import {type EventSubscription} from '../vendor/emitter/EventEmitter'; @@ -45,6 +43,7 @@ class BugReporting { if (!BugReporting._subscription) { BugReporting._subscription = RCTDeviceEventEmitter.addListener( 'collectBugExtraData', + // $FlowFixMe[method-unbinding] BugReporting.collectExtraData, null, ); @@ -54,6 +53,7 @@ class BugReporting { if (!BugReporting._redboxSubscription) { BugReporting._redboxSubscription = RCTDeviceEventEmitter.addListener( 'collectRedBoxExtraData', + // $FlowFixMe[method-unbinding] BugReporting.collectExtraData, null, ); diff --git a/Libraries/BugReporting/getReactData.js b/Libraries/BugReporting/getReactData.js index cc9bc55f0cc394..c851f76f993f6a 100644 --- a/Libraries/BugReporting/getReactData.js +++ b/Libraries/BugReporting/getReactData.js @@ -14,7 +14,7 @@ * Convert a react internal instance to a sanitized data object. * * This is shamelessly stolen from react-devtools: - * https://github.com/facebook/react-devtools/blob/master/backend/getData.js + * https://github.com/facebook/react-devtools/blob/HEAD/backend/getData.js */ function getData(element: Object): Object { let children = null; @@ -162,7 +162,7 @@ function copyWithSetImpl(obj, path, idx, value) { } const key = path[idx]; const updated = Array.isArray(obj) ? obj.slice() : {...obj}; - // $FlowFixMe number or string is fine here + // $FlowFixMe[incompatible-use] number or string is fine here updated[key] = copyWithSetImpl(obj[key], path, idx + 1, value); return updated; } diff --git a/Libraries/Components/AccessibilityInfo/AccessibilityInfo.android.js b/Libraries/Components/AccessibilityInfo/AccessibilityInfo.android.js deleted file mode 100644 index 138912dcf14ac7..00000000000000 --- a/Libraries/Components/AccessibilityInfo/AccessibilityInfo.android.js +++ /dev/null @@ -1,165 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its 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 strict'; - -import RCTDeviceEventEmitter from '../../EventEmitter/RCTDeviceEventEmitter'; -import UIManager from '../../ReactNative/UIManager'; -import NativeAccessibilityInfo from './NativeAccessibilityInfo'; - -const REDUCE_MOTION_EVENT = 'reduceMotionDidChange'; -const TOUCH_EXPLORATION_EVENT = 'touchExplorationDidChange'; - -type ChangeEventName = $Keys<{ - change: string, - reduceMotionChanged: string, - screenReaderChanged: string, - ... -}>; - -const _subscriptions = new Map(); - -/** - * Sometimes it's useful to know whether or not the device has a screen reader - * that is currently active. The `AccessibilityInfo` API is designed for this - * purpose. You can use it to query the current state of the screen reader as - * well as to register to be notified when the state of the screen reader - * changes. - * - * See https://reactnative.dev/docs/accessibilityinfo.html - */ - -const AccessibilityInfo = { - /** - * iOS only - */ - isBoldTextEnabled: function(): Promise { - return Promise.resolve(false); - }, - - /** - * iOS only - */ - isGrayscaleEnabled: function(): Promise { - return Promise.resolve(false); - }, - - /** - * macOS only - */ - isHighContrastEnabled: function(): Promise { - return Promise.resolve(false); - }, - - /** - * iOS only - */ - isInvertColorsEnabled: function(): Promise { - return Promise.resolve(false); - }, - - isReduceMotionEnabled: function(): Promise { - return new Promise((resolve, reject) => { - if (NativeAccessibilityInfo) { - NativeAccessibilityInfo.isReduceMotionEnabled(resolve); - } else { - reject(false); - } - }); - }, - - /** - * iOS only - */ - isReduceTransparencyEnabled: function(): Promise { - return Promise.resolve(false); - }, - - isScreenReaderEnabled: function(): Promise { - return new Promise((resolve, reject) => { - if (NativeAccessibilityInfo) { - NativeAccessibilityInfo.isTouchExplorationEnabled(resolve); - } else { - reject(false); - } - }); - }, - - /** - * Deprecated - * - * Same as `isScreenReaderEnabled` - */ - // $FlowFixMe[unsafe-getters-setters] - get fetch(): () => Promise { - console.warn( - 'AccessibilityInfo.fetch is deprecated, call AccessibilityInfo.isScreenReaderEnabled instead', - ); - return this.isScreenReaderEnabled; - }, - - addEventListener: function(eventName: ChangeEventName, handler: T): void { - let listener; - - if (eventName === 'change' || eventName === 'screenReaderChanged') { - listener = RCTDeviceEventEmitter.addListener( - TOUCH_EXPLORATION_EVENT, - handler, - ); - } else if (eventName === 'reduceMotionChanged') { - listener = RCTDeviceEventEmitter.addListener( - REDUCE_MOTION_EVENT, - handler, - ); - } - - // $FlowFixMe[escaped-generic] - _subscriptions.set(handler, listener); - }, - - removeEventListener: function( - eventName: ChangeEventName, - handler: T, - ): void { - // $FlowFixMe[escaped-generic] - const listener = _subscriptions.get(handler); - if (!listener) { - return; - } - listener.remove(); - // $FlowFixMe[escaped-generic] - _subscriptions.delete(handler); - }, - - /** - * Set accessibility focus to a react component. - * - * See https://reactnative.dev/docs/accessibilityinfo.html#setaccessibilityfocus - */ - setAccessibilityFocus: function(reactTag: number): void { - UIManager.sendAccessibilityEvent( - reactTag, - UIManager.getConstants().AccessibilityEventTypes.typeViewFocused, - ); - }, - - /** - * Post a string to be announced by the screen reader. - * - * See https://reactnative.dev/docs/accessibilityinfo.html#announceforaccessibility - */ - announceForAccessibility: function(announcement: string): void { - if (NativeAccessibilityInfo) { - NativeAccessibilityInfo.announceForAccessibility(announcement); - } - }, -}; - -module.exports = AccessibilityInfo; diff --git a/Libraries/Components/AccessibilityInfo/AccessibilityInfo.ios.js b/Libraries/Components/AccessibilityInfo/AccessibilityInfo.ios.js deleted file mode 100644 index fa73f4d692151a..00000000000000 --- a/Libraries/Components/AccessibilityInfo/AccessibilityInfo.ios.js +++ /dev/null @@ -1,280 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its 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 strict'; - -import RCTDeviceEventEmitter from '../../EventEmitter/RCTDeviceEventEmitter'; -import NativeAccessibilityManager from './NativeAccessibilityManager'; - -const CHANGE_EVENT_NAME = { - announcementFinished: 'announcementFinished', - boldTextChanged: 'boldTextChanged', - grayscaleChanged: 'grayscaleChanged', - invertColorsChanged: 'invertColorsChanged', - reduceMotionChanged: 'reduceMotionChanged', - reduceTransparencyChanged: 'reduceTransparencyChanged', - screenReaderChanged: 'screenReaderChanged', -}; - -type ChangeEventName = $Keys<{ - announcementFinished: string, - boldTextChanged: string, - change: string, - grayscaleChanged: string, - invertColorsChanged: string, - reduceMotionChanged: string, - reduceTransparencyChanged: string, - screenReaderChanged: string, - ... -}>; - -const _subscriptions = new Map(); - -/** - * Sometimes it's useful to know whether or not the device has a screen reader - * that is currently active. The `AccessibilityInfo` API is designed for this - * purpose. You can use it to query the current state of the screen reader as - * well as to register to be notified when the state of the screen reader - * changes. - * - * See https://reactnative.dev/docs/accessibilityinfo.html - */ -const AccessibilityInfo = { - /** - * Query whether bold text is currently enabled. - * - * Returns a promise which resolves to a boolean. - * The result is `true` when bold text is enabled and `false` otherwise. - * - * See https://reactnative.dev/docs/accessibilityinfo.html#isBoldTextEnabled - */ - isBoldTextEnabled: function(): Promise { - return new Promise((resolve, reject) => { - if (NativeAccessibilityManager) { - NativeAccessibilityManager.getCurrentBoldTextState(resolve, reject); - } else { - reject(reject); - } - }); - }, - - /** - * Query whether grayscale is currently enabled. - * - * Returns a promise which resolves to a boolean. - * The result is `true` when grayscale is enabled and `false` otherwise. - * - * See https://reactnative.dev/docs/accessibilityinfo.html#isGrayscaleEnabled - */ - isGrayscaleEnabled: function(): Promise { - return new Promise((resolve, reject) => { - if (NativeAccessibilityManager) { - NativeAccessibilityManager.getCurrentGrayscaleState(resolve, reject); - } else { - reject(reject); - } - }); - }, - - /** - * macOS only - */ - isHighContrastEnabled: function(): Promise { - return Promise.resolve(false); - }, - - /** - * Query whether inverted colors are currently enabled. - * - * Returns a promise which resolves to a boolean. - * The result is `true` when invert color is enabled and `false` otherwise. - * - * See https://reactnative.dev/docs/accessibilityinfo.html#isInvertColorsEnabled - */ - isInvertColorsEnabled: function(): Promise { - return new Promise((resolve, reject) => { - if (NativeAccessibilityManager) { - NativeAccessibilityManager.getCurrentInvertColorsState(resolve, reject); - } else { - reject(reject); - } - }); - }, - - /** - * Query whether reduced motion is currently enabled. - * - * Returns a promise which resolves to a boolean. - * The result is `true` when a reduce motion is enabled and `false` otherwise. - * - * See https://reactnative.dev/docs/accessibilityinfo.html#isReduceMotionEnabled - */ - isReduceMotionEnabled: function(): Promise { - return new Promise((resolve, reject) => { - if (NativeAccessibilityManager) { - NativeAccessibilityManager.getCurrentReduceMotionState(resolve, reject); - } else { - reject(reject); - } - }); - }, - - /** - * Query whether reduced transparency is currently enabled. - * - * Returns a promise which resolves to a boolean. - * The result is `true` when a reduce transparency is enabled and `false` otherwise. - * - * See https://reactnative.dev/docs/accessibilityinfo.html#isReduceTransparencyEnabled - */ - isReduceTransparencyEnabled: function(): Promise { - return new Promise((resolve, reject) => { - if (NativeAccessibilityManager) { - NativeAccessibilityManager.getCurrentReduceTransparencyState( - resolve, - reject, - ); - } else { - reject(reject); - } - }); - }, - - /** - * Query whether a screen reader is currently enabled. - * - * Returns a promise which resolves to a boolean. - * The result is `true` when a screen reader is enabled and `false` otherwise. - * - * See https://reactnative.dev/docs/accessibilityinfo.html#isScreenReaderEnabled - */ - isScreenReaderEnabled: function(): Promise { - return new Promise((resolve, reject) => { - if (NativeAccessibilityManager) { - NativeAccessibilityManager.getCurrentVoiceOverState(resolve, reject); - } else { - reject(reject); - } - }); - }, - - /** - * Deprecated - * - * Same as `isScreenReaderEnabled` - */ - // $FlowFixMe[unsafe-getters-setters] - get fetch(): $FlowFixMe { - console.warn( - 'AccessibilityInfo.fetch is deprecated, call AccessibilityInfo.isScreenReaderEnabled instead', - ); - return this.isScreenReaderEnabled; - }, - - /** - * Add an event handler. Supported events: - * - * - `boldTextChanged`: iOS-only event. Fires when the state of the bold text toggle changes. - * The argument to the event handler is a boolean. The boolean is `true` when a bold text - * is enabled and `false` otherwise. - * - `grayscaleChanged`: iOS-only event. Fires when the state of the gray scale toggle changes. - * The argument to the event handler is a boolean. The boolean is `true` when a gray scale - * is enabled and `false` otherwise. - * - `invertColorsChanged`: iOS-only event. Fires when the state of the invert colors toggle - * changes. The argument to the event handler is a boolean. The boolean is `true` when a invert - * colors is enabled and `false` otherwise. - * - `reduceMotionChanged`: Fires when the state of the reduce motion toggle changes. - * The argument to the event handler is a boolean. The boolean is `true` when a reduce - * motion is enabled (or when "Transition Animation Scale" in "Developer options" is - * "Animation off") and `false` otherwise. - * - `reduceTransparencyChanged`: iOS-only event. Fires when the state of the reduce transparency - * toggle changes. The argument to the event handler is a boolean. The boolean is `true` - * when a reduce transparency is enabled and `false` otherwise. - * - `screenReaderChanged`: Fires when the state of the screen reader changes. The argument - * to the event handler is a boolean. The boolean is `true` when a screen - * reader is enabled and `false` otherwise. - * - `announcementFinished`: iOS-only event. Fires when the screen reader has - * finished making an announcement. The argument to the event handler is a - * dictionary with these keys: - * - `announcement`: The string announced by the screen reader. - * - `success`: A boolean indicating whether the announcement was - * successfully made. - * - * See https://reactnative.dev/docs/accessibilityinfo.html#addeventlistener - */ - addEventListener: function( - eventName: ChangeEventName, - handler: T, - ): {remove: () => void} { - let listener; - - if (eventName === 'change') { - listener = RCTDeviceEventEmitter.addListener( - CHANGE_EVENT_NAME.screenReaderChanged, - handler, - ); - } else if (CHANGE_EVENT_NAME[eventName]) { - listener = RCTDeviceEventEmitter.addListener(eventName, handler); - } - - // $FlowFixMe[escaped-generic] - _subscriptions.set(handler, listener); - return { - remove: AccessibilityInfo.removeEventListener.bind( - null, - eventName, - handler, - ), - }; - }, - - /** - * Set accessibility focus to a react component. - * - * See https://reactnative.dev/docs/accessibilityinfo.html#setaccessibilityfocus - */ - setAccessibilityFocus: function(reactTag: number): void { - if (NativeAccessibilityManager) { - NativeAccessibilityManager.setAccessibilityFocus(reactTag); - } - }, - - /** - * Post a string to be announced by the screen reader. - * - * See https://reactnative.dev/docs/accessibilityinfo.html#announceforaccessibility - */ - announceForAccessibility: function(announcement: string): void { - if (NativeAccessibilityManager) { - NativeAccessibilityManager.announceForAccessibility(announcement); - } - }, - - /** - * Remove an event handler. - * - * See https://reactnative.dev/docs/accessibilityinfo.html#removeeventlistener - */ - removeEventListener: function( - eventName: ChangeEventName, - handler: T, - ): void { - // $FlowFixMe[escaped-generic] - const listener = _subscriptions.get(handler); - if (!listener) { - return; - } - listener.remove(); - // $FlowFixMe[escaped-generic] - _subscriptions.delete(handler); - }, -}; - -module.exports = AccessibilityInfo; diff --git a/Libraries/Components/AccessibilityInfo/AccessibilityInfo.js b/Libraries/Components/AccessibilityInfo/AccessibilityInfo.js new file mode 100644 index 00000000000000..8c3f38204d12fa --- /dev/null +++ b/Libraries/Components/AccessibilityInfo/AccessibilityInfo.js @@ -0,0 +1,372 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + */ + +import RCTDeviceEventEmitter from '../../EventEmitter/RCTDeviceEventEmitter'; +import {sendAccessibilityEvent} from '../../Renderer/shims/ReactNative'; +import type {HostComponent} from '../../Renderer/shims/ReactNativeTypes'; +import Platform from '../../Utilities/Platform'; +import type EventEmitter from '../../vendor/emitter/EventEmitter'; +import type {EventSubscription} from '../../vendor/emitter/EventEmitter'; +import NativeAccessibilityInfoAndroid from './NativeAccessibilityInfo'; +import NativeAccessibilityManagerApple from './NativeAccessibilityManager'; +import legacySendAccessibilityEvent from './legacySendAccessibilityEvent'; +import type {ElementRef} from 'react'; + +// Events that are only supported on iOS. +type AccessibilityEventDefinitionsIOS = { + announcementFinished: [{announcement: string, success: boolean}], + boldTextChanged: [boolean], + grayscaleChanged: [boolean], + invertColorsChanged: [boolean], + reduceTransparencyChanged: [boolean], +}; + +type AccessibilityEventDefinitions = { + ...AccessibilityEventDefinitionsIOS, + highContrastChanged: [boolean], // TODO(macOS GH#774) - highContrastChanged is used on macOS + change: [boolean], // screenReaderChanged + reduceMotionChanged: [boolean], + screenReaderChanged: [boolean], +}; + +type AccessibilityEventTypes = 'click' | 'focus'; + +// Mapping of public event names to platform-specific event names. +const EventNames: Map<$Keys, string> = + Platform.OS === 'android' + ? new Map([ + ['change', 'touchExplorationDidChange'], + ['reduceMotionChanged', 'reduceMotionDidChange'], + ['screenReaderChanged', 'touchExplorationDidChange'], + ]) + : new Map([ + ['announcementFinished', 'announcementFinished'], + ['boldTextChanged', 'boldTextChanged'], + ['change', 'screenReaderChanged'], + ['grayscaleChanged', 'grayscaleChanged'], + ['highContrastChanged', 'highContrastChanged'], + ['invertColorsChanged', 'invertColorsChanged'], + ['reduceMotionChanged', 'reduceMotionChanged'], + ['reduceTransparencyChanged', 'reduceTransparencyChanged'], + ['screenReaderChanged', 'screenReaderChanged'], + ]); + +/** + * Sometimes it's useful to know whether or not the device has a screen reader + * that is currently active. The `AccessibilityInfo` API is designed for this + * purpose. You can use it to query the current state of the screen reader as + * well as to register to be notified when the state of the screen reader + * changes. + * + * See https://reactnative.dev/docs/accessibilityinfo.html + */ +const AccessibilityInfo = { + /** + * Query whether bold text is currently enabled. + * + * Returns a promise which resolves to a boolean. + * The result is `true` when bold text is enabled and `false` otherwise. + * + * See https://reactnative.dev/docs/accessibilityinfo.html#isBoldTextEnabled + */ + isBoldTextEnabled(): Promise { + if (Platform.OS === 'android') { + return Promise.resolve(false); + } else { + return new Promise((resolve, reject) => { + if (NativeAccessibilityManagerApple != null) { + NativeAccessibilityManagerApple.getCurrentBoldTextState( + resolve, + reject, + ); + } else { + reject(null); + } + }); + } + }, + + /** + * Query whether grayscale is currently enabled. + * + * Returns a promise which resolves to a boolean. + * The result is `true` when grayscale is enabled and `false` otherwise. + * + * See https://reactnative.dev/docs/accessibilityinfo.html#isGrayscaleEnabled + */ + isGrayscaleEnabled(): Promise { + if (Platform.OS === 'android') { + return Promise.resolve(false); + } else { + return new Promise((resolve, reject) => { + if (NativeAccessibilityManagerApple != null) { + NativeAccessibilityManagerApple.getCurrentGrayscaleState( + resolve, + reject, + ); + } else { + reject(null); + } + }); + } + }, + + /** + * macOS only + */ + isHighContrastEnabled: function(): Promise { + if (Platform.OS === 'macos') { + return new Promise((resolve, reject) => { + if (NativeAccessibilityManagerApple) { + NativeAccessibilityManagerApple.getCurrentHighContrastState( + resolve, + reject, + ); + } else { + reject(reject); + } + }); + } else { + return Promise.resolve(false); + } + }, + + /** + * Query whether inverted colors are currently enabled. + * + * Returns a promise which resolves to a boolean. + * The result is `true` when invert color is enabled and `false` otherwise. + * + * See https://reactnative.dev/docs/accessibilityinfo.html#isInvertColorsEnabled + */ + isInvertColorsEnabled(): Promise { + if (Platform.OS === 'android') { + return Promise.resolve(false); + } else { + return new Promise((resolve, reject) => { + if (NativeAccessibilityManagerApple != null) { + NativeAccessibilityManagerApple.getCurrentInvertColorsState( + resolve, + reject, + ); + } else { + reject(null); + } + }); + } + }, + + /** + * Query whether reduced motion is currently enabled. + * + * Returns a promise which resolves to a boolean. + * The result is `true` when a reduce motion is enabled and `false` otherwise. + * + * See https://reactnative.dev/docs/accessibilityinfo.html#isReduceMotionEnabled + */ + isReduceMotionEnabled(): Promise { + return new Promise((resolve, reject) => { + if (Platform.OS === 'android') { + if (NativeAccessibilityInfoAndroid != null) { + NativeAccessibilityInfoAndroid.isReduceMotionEnabled(resolve); + } else { + reject(null); + } + } else { + if (NativeAccessibilityManagerApple != null) { + NativeAccessibilityManagerApple.getCurrentReduceMotionState( + resolve, + reject, + ); + } else { + reject(null); + } + } + }); + }, + + /** + * Query whether reduced transparency is currently enabled. + * + * Returns a promise which resolves to a boolean. + * The result is `true` when a reduce transparency is enabled and `false` otherwise. + * + * See https://reactnative.dev/docs/accessibilityinfo.html#isReduceTransparencyEnabled + */ + isReduceTransparencyEnabled(): Promise { + if (Platform.OS === 'android') { + return Promise.resolve(false); + } else { + return new Promise((resolve, reject) => { + if (NativeAccessibilityManagerApple != null) { + NativeAccessibilityManagerApple.getCurrentReduceTransparencyState( + resolve, + reject, + ); + } else { + reject(null); + } + }); + } + }, + + /** + * Query whether a screen reader is currently enabled. + * + * Returns a promise which resolves to a boolean. + * The result is `true` when a screen reader is enabled and `false` otherwise. + * + * See https://reactnative.dev/docs/accessibilityinfo.html#isScreenReaderEnabled + */ + isScreenReaderEnabled(): Promise { + return new Promise((resolve, reject) => { + if (Platform.OS === 'android') { + if (NativeAccessibilityInfoAndroid != null) { + NativeAccessibilityInfoAndroid.isTouchExplorationEnabled(resolve); + } else { + reject(null); + } + } else { + if (NativeAccessibilityManagerApple != null) { + NativeAccessibilityManagerApple.getCurrentVoiceOverState( + resolve, + reject, + ); + } else { + reject(null); + } + } + }); + }, + + /** + * Add an event handler. Supported events: + * + * - `reduceMotionChanged`: Fires when the state of the reduce motion toggle changes. + * The argument to the event handler is a boolean. The boolean is `true` when a reduce + * motion is enabled (or when "Transition Animation Scale" in "Developer options" is + * "Animation off") and `false` otherwise. + * - `screenReaderChanged`: Fires when the state of the screen reader changes. The argument + * to the event handler is a boolean. The boolean is `true` when a screen + * reader is enabled and `false` otherwise. + * + * These events are only supported on iOS: + * + * - `boldTextChanged`: iOS-only event. Fires when the state of the bold text toggle changes. + * The argument to the event handler is a boolean. The boolean is `true` when a bold text + * is enabled and `false` otherwise. + * - `grayscaleChanged`: iOS-only event. Fires when the state of the gray scale toggle changes. + * The argument to the event handler is a boolean. The boolean is `true` when a gray scale + * is enabled and `false` otherwise. + * - `invertColorsChanged`: iOS-only event. Fires when the state of the invert colors toggle + * changes. The argument to the event handler is a boolean. The boolean is `true` when a invert + * colors is enabled and `false` otherwise. + * - `reduceTransparencyChanged`: iOS-only event. Fires when the state of the reduce transparency + * toggle changes. The argument to the event handler is a boolean. The boolean is `true` + * when a reduce transparency is enabled and `false` otherwise. + * - `announcementFinished`: iOS-only event. Fires when the screen reader has + * finished making an announcement. The argument to the event handler is a + * dictionary with these keys: + * - `announcement`: The string announced by the screen reader. + * - `success`: A boolean indicating whether the announcement was + * successfully made. + * + * See https://reactnative.dev/docs/accessibilityinfo.html#addeventlistener + */ + addEventListener>( + eventName: K, + handler: (...$ElementType) => void, + ): EventSubscription { + const deviceEventName = EventNames.get(eventName); + return deviceEventName == null + ? {remove(): void {}} + : RCTDeviceEventEmitter.addListener(deviceEventName, handler); + }, + + /** + * Set accessibility focus to a React component. + * + * See https://reactnative.dev/docs/accessibilityinfo.html#setaccessibilityfocus + */ + setAccessibilityFocus(reactTag: number): void { + legacySendAccessibilityEvent(reactTag, 'focus'); + }, + + /** + * Send a named accessibility event to a HostComponent. + */ + sendAccessibilityEvent_unstable( + handle: ElementRef>, + eventType: AccessibilityEventTypes, + ) { + // iOS only supports 'focus' event types + if (Platform.OS === 'ios' && eventType === 'click') { + return; + } + // route through React renderer to distinguish between Fabric and non-Fabric handles + sendAccessibilityEvent(handle, eventType); + }, + + /** + * Post a string to be announced by the screen reader. + * + * See https://reactnative.dev/docs/accessibilityinfo.html#announceforaccessibility + */ + announceForAccessibility(announcement: string): void { + if (Platform.OS === 'android') { + NativeAccessibilityInfoAndroid?.announceForAccessibility(announcement); + } else { + NativeAccessibilityManagerApple?.announceForAccessibility(announcement); + } + }, + + /** + * @deprecated Use `remove` on the EventSubscription from `addEventListener`. + */ + removeEventListener>( + eventName: K, + handler: (...$ElementType) => void, + ): void { + // NOTE: This will report a deprecation notice via `console.error`. + const deviceEventName = EventNames.get(eventName); + if (deviceEventName != null) { + // $FlowIgnore[incompatible-cast] + (RCTDeviceEventEmitter: EventEmitter<$FlowFixMe>).removeListener( + 'deviceEventName', + // $FlowFixMe[invalid-tuple-arity] + handler, + ); + } + }, + + /** + * Get the recommended timeout for changes to the UI needed by this user. + * + * See https://reactnative.dev/docs/accessibilityinfo.html#getrecommendedtimeoutmillis + */ + getRecommendedTimeoutMillis(originalTimeout: number): Promise { + if (Platform.OS === 'android') { + return new Promise((resolve, reject) => { + if (NativeAccessibilityInfoAndroid?.getRecommendedTimeoutMillis) { + NativeAccessibilityInfoAndroid.getRecommendedTimeoutMillis( + originalTimeout, + resolve, + ); + } else { + resolve(originalTimeout); + } + }); + } else { + return Promise.resolve(originalTimeout); + } + }, +}; + +export default AccessibilityInfo; diff --git a/Libraries/Components/AccessibilityInfo/AccessibilityInfo.macos.js b/Libraries/Components/AccessibilityInfo/AccessibilityInfo.macos.js deleted file mode 100644 index 910fbc1f047e5d..00000000000000 --- a/Libraries/Components/AccessibilityInfo/AccessibilityInfo.macos.js +++ /dev/null @@ -1,231 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its 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 - */ - -// TODO(macOS GH#774) - -'use strict'; - -const Promise = require('../../Promise'); -import RCTDeviceEventEmitter from '../../EventEmitter/RCTDeviceEventEmitter'; - -import NativeAccessibilityManager from './NativeAccessibilityManager'; - -const CHANGE_EVENT_NAME = { - highContrastChanged: 'highContrastChanged', - invertColorsChanged: 'invertColorsChanged', - reduceMotionChanged: 'reduceMotionChanged', - reduceTransparencyChanged: 'reduceTransparencyChanged', - screenReaderChanged: 'screenReaderChanged', -}; - -type ChangeEventName = $Keys<{ - change: string, - highContrastChanged: string, - invertColorsChanged: string, - reduceMotionChanged: string, - reduceTransparencyChanged: string, - screenReaderChanged: string, -}>; - -const _subscriptions = new Map(); - -/** - * Sometimes it's useful to know whether or not the device has a screen reader - * that is currently active. The `AccessibilityInfo` API is designed for this - * purpose. You can use it to query the current state of the screen reader as - * well as to register to be notified when the state of the screen reader - * changes. - * - * See http://facebook.github.io/react-native/docs/accessibilityinfo.html - */ -const AccessibilityInfo = { - /** - * iOS only - */ - isBoldTextEnabled: function(): Promise { - return Promise.resolve(false); - }, - - /** - * iOS only - */ - isGrayscaleEnabled: function(): Promise { - return Promise.resolve(false); - }, - - /** - * Query whether high contrast is currently enabled. - * - * Returns a promise which resolves to a boolean. - * The result is `true` when invert color is enabled and `false` otherwise. - */ - isHighContrastEnabled: function(): Promise { - return new Promise((resolve, reject) => { - if (NativeAccessibilityManager) { - NativeAccessibilityManager.getCurrentHighContrastState(resolve, reject); - } else { - reject(reject); - } - }); - }, - - /** - * Query whether inverted colors are currently enabled. - * - * Returns a promise which resolves to a boolean. - * The result is `true` when invert color is enabled and `false` otherwise. - * - * See http://facebook.github.io/react-native/docs/accessibilityinfo.html#isInvertColorsEnabled - */ - isInvertColorsEnabled: function(): Promise { - return new Promise((resolve, reject) => { - if (NativeAccessibilityManager) { - NativeAccessibilityManager.getCurrentInvertColorsState(resolve, reject); - } else { - reject(reject); - } - }); - }, - - /** - * Query whether reduced motion is currently enabled. - * - * Returns a promise which resolves to a boolean. - * The result is `true` when a reduce motion is enabled and `false` otherwise. - * - * See http://facebook.github.io/react-native/docs/accessibilityinfo.html#isReduceMotionEnabled - */ - isReduceMotionEnabled: function(): Promise { - return new Promise((resolve, reject) => { - if (NativeAccessibilityManager) { - NativeAccessibilityManager.getCurrentReduceMotionState(resolve, reject); - } else { - reject(reject); - } - }); - }, - - /** - * Query whether reduced transparency is currently enabled. - * - * Returns a promise which resolves to a boolean. - * The result is `true` when a reduce transparency is enabled and `false` otherwise. - * - * See http://facebook.github.io/react-native/docs/accessibilityinfo.html#isReduceTransparencyEnabled - */ - isReduceTransparencyEnabled: function(): Promise { - return new Promise((resolve, reject) => { - if (NativeAccessibilityManager) { - NativeAccessibilityManager.getCurrentReduceTransparencyState( - resolve, - reject, - ); - } else { - reject(reject); - } - }); - }, - - /** - * Query whether a screen reader is currently enabled. - * - * Returns a promise which resolves to a boolean. - * The result is `true` when a screen reader is enabled and `false` otherwise. - * - * See http://facebook.github.io/react-native/docs/accessibilityinfo.html#isScreenReaderEnabled - */ - isScreenReaderEnabled: function(): Promise { - return new Promise((resolve, reject) => { - if (NativeAccessibilityManager) { - NativeAccessibilityManager.getCurrentVoiceOverState(resolve, reject); - } else { - reject(reject); - } - }); - }, - - /** - * Deprecated - * - * Same as `isScreenReaderEnabled` - */ - get fetch(): $FlowFixMe { - console.warn( - 'AccessibilityInfo.fetch is deprecated, call Accessibility.isScreenReaderEnabled instead', - ); - return this.isScreenReaderEnabled; - }, - - addEventListener: function( - eventName: ChangeEventName, - handler: Function, - ): Object { - let listener; - - if (eventName === 'change') { - listener = RCTDeviceEventEmitter.addListener( - CHANGE_EVENT_NAME.screenReaderChanged, - handler, - ); - } else if (CHANGE_EVENT_NAME[eventName]) { - listener = RCTDeviceEventEmitter.addListener(eventName, handler); - } - - _subscriptions.set(handler, listener); - return { - remove: AccessibilityInfo.removeEventListener.bind( - null, - eventName, - handler, - ), - }; - }, - - /** - * Set accessibility focus to a react component. - * - * See http://facebook.github.io/react-native/docs/accessibilityinfo.html#setaccessibilityfocus - */ - setAccessibilityFocus: function(reactTag: number): void { - if (NativeAccessibilityManager) { - NativeAccessibilityManager.setAccessibilityFocus(reactTag); - } - }, - - /** - * Post a string to be announced by the screen reader. - * - * See http://facebook.github.io/react-native/docs/accessibilityinfo.html#announceforaccessibility - */ - announceForAccessibility: function(announcement: string): void { - if (NativeAccessibilityManager) { - NativeAccessibilityManager.announceForAccessibility(announcement); - } - }, - - /** - * Remove an event handler. - * - * See http://facebook.github.io/react-native/docs/accessibilityinfo.html#removeeventlistener - */ - removeEventListener: function( - eventName: ChangeEventName, - handler: Function, - ): void { - const listener = _subscriptions.get(handler); - if (!listener) { - return; - } - listener.remove(); - _subscriptions.delete(handler); - }, -}; - -module.exports = AccessibilityInfo; diff --git a/Libraries/Components/AccessibilityInfo/NativeAccessibilityInfo.js b/Libraries/Components/AccessibilityInfo/NativeAccessibilityInfo.js index 03484f2158c10d..916667aa5c2c0a 100644 --- a/Libraries/Components/AccessibilityInfo/NativeAccessibilityInfo.js +++ b/Libraries/Components/AccessibilityInfo/NativeAccessibilityInfo.js @@ -8,8 +8,6 @@ * @format */ -'use strict'; - import type {TurboModule} from '../../TurboModule/RCTExport'; import * as TurboModuleRegistry from '../../TurboModule/TurboModuleRegistry'; @@ -22,6 +20,10 @@ export interface Spec extends TurboModule { ) => void; +setAccessibilityFocus: (reactTag: number) => void; +announceForAccessibility: (announcement: string) => void; + +getRecommendedTimeoutMillis?: ( + mSec: number, + onSuccess: (recommendedTimeoutMillis: number) => void, + ) => void; } export default (TurboModuleRegistry.get('AccessibilityInfo'): ?Spec); diff --git a/Libraries/Components/AccessibilityInfo/NativeAccessibilityManager.js b/Libraries/Components/AccessibilityInfo/NativeAccessibilityManager.js index 6fc0f56e13e7cd..e8e95f22c3abc3 100644 --- a/Libraries/Components/AccessibilityInfo/NativeAccessibilityManager.js +++ b/Libraries/Components/AccessibilityInfo/NativeAccessibilityManager.js @@ -8,8 +8,6 @@ * @format */ -'use strict'; - import type {TurboModule} from '../../TurboModule/RCTExport'; import * as TurboModuleRegistry from '../../TurboModule/TurboModuleRegistry'; diff --git a/Libraries/Components/AccessibilityInfo/legacySendAccessibilityEvent.android.js b/Libraries/Components/AccessibilityInfo/legacySendAccessibilityEvent.android.js new file mode 100644 index 00000000000000..3bb1c554950d50 --- /dev/null +++ b/Libraries/Components/AccessibilityInfo/legacySendAccessibilityEvent.android.js @@ -0,0 +1,35 @@ +/** + * Copyright (c) Facebook, Inc. and its 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 + */ + +import UIManager from '../../ReactNative/UIManager'; + +/** + * This is a function exposed to the React Renderer that can be used by the + * pre-Fabric renderer to emit accessibility events to pre-Fabric nodes. + */ +function legacySendAccessibilityEvent( + reactTag: number, + eventType: string, +): void { + if (eventType === 'focus') { + UIManager.sendAccessibilityEvent( + reactTag, + UIManager.getConstants().AccessibilityEventTypes.typeViewFocused, + ); + } + if (eventType === 'click') { + UIManager.sendAccessibilityEvent( + reactTag, + UIManager.getConstants().AccessibilityEventTypes.typeViewClicked, + ); + } +} + +module.exports = legacySendAccessibilityEvent; diff --git a/Libraries/Components/AccessibilityInfo/legacySendAccessibilityEvent.ios.js b/Libraries/Components/AccessibilityInfo/legacySendAccessibilityEvent.ios.js new file mode 100644 index 00000000000000..950336d1101c44 --- /dev/null +++ b/Libraries/Components/AccessibilityInfo/legacySendAccessibilityEvent.ios.js @@ -0,0 +1,26 @@ +/** + * Copyright (c) Facebook, Inc. and its 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 + */ + +import NativeAccessibilityManager from './NativeAccessibilityManager'; + +/** + * This is a function exposed to the React Renderer that can be used by the + * pre-Fabric renderer to emit accessibility events to pre-Fabric nodes. + */ +function legacySendAccessibilityEvent( + reactTag: number, + eventType: string, +): void { + if (eventType === 'focus' && NativeAccessibilityManager) { + NativeAccessibilityManager.setAccessibilityFocus(reactTag); + } +} + +module.exports = legacySendAccessibilityEvent; diff --git a/Libraries/Components/AccessibilityInfo/legacySendAccessibilityEvent.macos.js b/Libraries/Components/AccessibilityInfo/legacySendAccessibilityEvent.macos.js new file mode 100644 index 00000000000000..831db39d1336c1 --- /dev/null +++ b/Libraries/Components/AccessibilityInfo/legacySendAccessibilityEvent.macos.js @@ -0,0 +1,16 @@ +/** + * Copyright (c) Facebook, Inc. and its 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 + */ + +// TODO(macOS GH#774) + +/* $FlowFixMe allow macOS to share iOS file */ +const legacySendAccessibilityEvent = require('./legacySendAccessibilityEvent.ios'); + +module.exports = legacySendAccessibilityEvent; diff --git a/Libraries/Components/ActivityIndicator/ActivityIndicator.js b/Libraries/Components/ActivityIndicator/ActivityIndicator.js index 52cbda4a7312e2..9100cf6483de2a 100644 --- a/Libraries/Components/ActivityIndicator/ActivityIndicator.js +++ b/Libraries/Components/ActivityIndicator/ActivityIndicator.js @@ -10,14 +10,12 @@ */ 'use strict'; - -const Platform = require('../../Utilities/Platform'); -const React = require('react'); -const StyleSheet = require('../../StyleSheet/StyleSheet'); -const View = require('../View/View'); +import * as React from 'react'; +import Platform from '../../Utilities/Platform'; +import StyleSheet, {type ColorValue} from '../../StyleSheet/StyleSheet'; +import View from '../View/View'; import type {HostComponent} from '../../Renderer/shims/ReactNativeTypes'; import type {ViewProps} from '../View/ViewPropTypes'; -import type {ColorValue} from '../../StyleSheet/StyleSheet'; const PlatformActivityIndicator = Platform.OS === 'android' @@ -62,8 +60,18 @@ type Props = $ReadOnly<{| size?: ?IndicatorSize, |}>; -const ActivityIndicator = (props: Props, forwardedRef?: any) => { - const {onLayout, style, size, ...restProps} = props; +const ActivityIndicator = ( + { + animating = true, + color = Platform.OS === 'ios' ? GRAY : null, + hidesWhenStopped = true, + onLayout, + size = 'small', + style, + ...restProps + }: Props, + forwardedRef?: any, +) => { let sizeStyle; let sizeProp; @@ -77,11 +85,14 @@ const ActivityIndicator = (props: Props, forwardedRef?: any) => { sizeProp = 'large'; break; default: - sizeStyle = {height: props.size, width: props.size}; + sizeStyle = {height: size, width: size}; break; } const nativeProps = { + animating, + color, + hidesWhenStopped, ...restProps, ref: forwardedRef, style: sizeStyle, @@ -98,10 +109,10 @@ const ActivityIndicator = (props: Props, forwardedRef?: any) => { onLayout={onLayout} style={StyleSheet.compose(styles.container, style)}> {Platform.OS === 'android' ? ( - // $FlowFixMe Flow doesn't know when this is the android component + // $FlowFixMe[prop-missing] Flow doesn't know when this is the android component ) : ( - /* $FlowFixMe(>=0.106.0 site=react_native_android_fb) This comment + /* $FlowFixMe[prop-missing] (>=0.106.0 site=react_native_android_fb) This comment * suppresses an error found when Flow v0.106 was deployed. To see the * error, delete this comment and run Flow. */ @@ -178,16 +189,6 @@ const ActivityIndicatorWithRef: React.AbstractComponent< > = React.forwardRef(ActivityIndicator); ActivityIndicatorWithRef.displayName = 'ActivityIndicator'; -/* $FlowFixMe(>=0.89.0 site=react_native_fb) This comment suppresses an error - * found when Flow v0.89 was deployed. To see the error, delete this comment - * and run Flow. */ -ActivityIndicatorWithRef.defaultProps = { - animating: true, - color: Platform.OS === 'ios' ? GRAY : null, - hidesWhenStopped: true, - size: 'small', -}; - const styles = StyleSheet.create({ container: { alignItems: 'center', diff --git a/Libraries/Components/ActivityIndicator/ActivityIndicatorViewNativeComponent.js b/Libraries/Components/ActivityIndicator/ActivityIndicatorViewNativeComponent.js index a7a95da5943c1d..3475cebeddeabd 100644 --- a/Libraries/Components/ActivityIndicator/ActivityIndicatorViewNativeComponent.js +++ b/Libraries/Components/ActivityIndicator/ActivityIndicatorViewNativeComponent.js @@ -8,8 +8,6 @@ * @flow strict-local */ -'use strict'; - import type {WithDefault} from '../../Types/CodegenTypes'; import type {ColorValue} from '../../StyleSheet/StyleSheet'; diff --git a/Libraries/Components/ActivityIndicator/__tests__/__snapshots__/ActivityIndicator-test.js.snap b/Libraries/Components/ActivityIndicator/__tests__/__snapshots__/ActivityIndicator-test.js.snap index d8316e939d13d1..4326d7f8ae20f3 100644 --- a/Libraries/Components/ActivityIndicator/__tests__/__snapshots__/ActivityIndicator-test.js.snap +++ b/Libraries/Components/ActivityIndicator/__tests__/__snapshots__/ActivityIndicator-test.js.snap @@ -2,9 +2,7 @@ exports[` should render as expected: should deep render when mocked (please verify output manually) 1`] = ` `; @@ -35,18 +33,14 @@ exports[` should render as expected: should deep render whe exports[` should render as expected: should shallow render as when mocked 1`] = ` `; exports[` should render as expected: should shallow render as when not mocked 1`] = ` `; diff --git a/Libraries/Components/Button.js b/Libraries/Components/Button.js index 9c712d492fc5fd..d47fbfb938c887 100644 --- a/Libraries/Components/Button.js +++ b/Libraries/Components/Button.js @@ -11,24 +11,24 @@ 'use strict'; -const Platform = require('../Utilities/Platform'); -const React = require('react'); -const StyleSheet = require('../StyleSheet/StyleSheet'); -const Text = require('../Text/Text'); -const TouchableNativeFeedback = require('./Touchable/TouchableNativeFeedback'); -const TouchableOpacity = require('./Touchable/TouchableOpacity'); -const View = require('./View/View'); - -const invariant = require('invariant'); - -import type {PressEvent, KeyEvent} from '../Types/CoreEventTypes'; +import * as React from 'react'; +import Platform from '../Utilities/Platform'; +import StyleSheet, {type ColorValue} from '../StyleSheet/StyleSheet'; +import Text from '../Text/Text'; +import TouchableNativeFeedback from './Touchable/TouchableNativeFeedback'; +import TouchableOpacity from './Touchable/TouchableOpacity'; +import View from './View/View'; +import invariant from 'invariant'; +import type {KeyEvent} from '../Types/CoreEventTypes'; // TODO(OSS Candidate ISS#2710739) import type {FocusEvent, BlurEvent} from './TextInput/TextInput'; // TODO(OSS Candidate ISS#2710739) -import type {ColorValue} from '../StyleSheet/StyleSheet'; + import type { AccessibilityActionEvent, AccessibilityActionInfo, AccessibilityRole, + AccessibilityState, } from './View/ViewAccessibility'; +import type {PressEvent} from '../Types/CoreEventTypes'; type ButtonProps = $ReadOnly<{| /** @@ -143,7 +143,6 @@ type ButtonProps = $ReadOnly<{| /** * Accessibility action handlers */ - accessibilityActions?: ?$ReadOnlyArray, onAccessibilityAction?: ?(event: AccessibilityActionEvent) => mixed, /** @@ -196,6 +195,14 @@ type ButtonProps = $ReadOnly<{| */ tooltip?: string, // ]TODO(OSS Candidate ISS#2710739) + + /** + * Accessibility props. + */ + accessible?: ?boolean, + accessibilityActions?: ?$ReadOnlyArray, + onAccessibilityAction?: ?(event: AccessibilityActionEvent) => mixed, + accessibilityState?: ?AccessibilityState, |}>; /** @@ -210,7 +217,7 @@ type ButtonProps = $ReadOnly<{| [button:examples]. [button:source]: - https://github.com/facebook/react-native/blob/master/Libraries/Components/Button.js + https://github.com/facebook/react-native/blob/HEAD/Libraries/Components/Button.js [button:examples]: https://js.coach/?menu%5Bcollections%5D=React%20Native&page=1&query=button @@ -313,10 +320,6 @@ class Button extends React.Component { render(): React.Node { const { accessibilityLabel, - accessibilityHint, // TODO(OSS Candidate ISS#2710739) - accessibilityRole, // TODO(OSS Candidate ISS#2710739) - accessibilityActions, // TODO(OSS Candidate ISS#2710739) - onAccessibilityAction, // TODO(OSS Candidate ISS#2710739) color, onPress, touchSoundDisabled, @@ -327,15 +330,19 @@ class Button extends React.Component { nextFocusLeft, nextFocusRight, nextFocusUp, - disabled, testID, onFocus, // TODO(OSS Candidate ISS#2710739) onBlur, // TODO(OSS Candidate ISS#2710739) - onKeyDown, - onKeyUp, - validKeysDown, - validKeysUp, + onKeyDown, // TODO(OSS Candidate ISS#2710739) + onKeyUp, // TODO(OSS Candidate ISS#2710739) + validKeysDown, // TODO(OSS Candidate ISS#2710739) + validKeysUp, // TODO(OSS Candidate ISS#2710739) tooltip, + accessible, + accessibilityActions, + accessibilityHint, // TODO(OSS Candidate ISS#2710739) + accessibilityRole, // TODO(OSS Candidate ISS#2710739) + onAccessibilityAction, } = this.props; const buttonStyles = [styles.button]; const textStyles = [styles.text]; @@ -349,12 +356,22 @@ class Button extends React.Component { buttonStyles.push({backgroundColor: color}); } } - const accessibilityState = {}; + + const disabled = + this.props.disabled != null + ? this.props.disabled + : this.props.accessibilityState?.disabled; + + const accessibilityState = + disabled !== this.props.accessibilityState?.disabled + ? {...this.props.accessibilityState, disabled} + : this.props.accessibilityState; + if (disabled) { buttonStyles.push(styles.buttonDisabled); textStyles.push(styles.textDisabled); - accessibilityState.disabled = true; } + invariant( typeof title === 'string', 'The title prop of a Button must be a string', @@ -363,14 +380,16 @@ class Button extends React.Component { Platform.OS === 'android' ? title.toUpperCase() : title; const Touchable = Platform.OS === 'android' ? TouchableNativeFeedback : TouchableOpacity; + return ( ; /** @@ -149,7 +153,7 @@ class DatePickerIOS extends React.Component { ref={picker => { this._picker = picker; }} - style={styles.datePickerIOS} + style={getHeight(props.pickerStyle, props.mode)} date={ props.date ? props.date.getTime() @@ -174,16 +178,51 @@ class DatePickerIOS extends React.Component { onChange={this._onChange} onStartShouldSetResponder={() => true} onResponderTerminationRequest={() => false} + pickerStyle={props.pickerStyle} /> ); } } +const inlineHeightForDatePicker = 318.5; +const inlineHeightForTimePicker = 49.5; +const compactHeight = 40; +const spinnerHeight = 216; + const styles = StyleSheet.create({ datePickerIOS: { - height: 216, + height: spinnerHeight, + }, + datePickerIOSCompact: { + height: compactHeight, + }, + datePickerIOSInline: { + height: inlineHeightForDatePicker + inlineHeightForTimePicker * 2, + }, + datePickerIOSInlineDate: { + height: inlineHeightForDatePicker + inlineHeightForTimePicker, + }, + datePickerIOSInlineTime: { + height: inlineHeightForTimePicker, }, }); +function getHeight(pickerStyle, mode) { + if (pickerStyle === 'compact') { + return styles.datePickerIOSCompact; + } + if (pickerStyle === 'inline') { + switch (mode) { + case 'date': + return styles.datePickerIOSInlineDate; + case 'time': + return styles.datePickerIOSInlineTime; + default: + return styles.datePickerIOSInline; + } + } + return styles.datePickerIOS; +} + module.exports = DatePickerIOS; diff --git a/Libraries/Components/DatePicker/RCTDatePickerNativeComponent.js b/Libraries/Components/DatePicker/RCTDatePickerNativeComponent.js index 99e5d92c051e17..e577f3ee37b4d9 100644 --- a/Libraries/Components/DatePicker/RCTDatePickerNativeComponent.js +++ b/Libraries/Components/DatePicker/RCTDatePickerNativeComponent.js @@ -8,8 +8,6 @@ * @flow strict-local */ -'use strict'; - import type {HostComponent} from '../../Renderer/shims/ReactNativeTypes'; import type {ViewProps} from '../View/ViewPropTypes'; import codegenNativeCommands from 'react-native/Libraries/Utilities/codegenNativeCommands'; @@ -39,6 +37,7 @@ type NativeProps = $ReadOnly<{| mode?: WithDefault<'date' | 'time' | 'datetime', 'date'>, onChange?: ?BubblingEventHandler, timeZoneOffsetInMinutes?: ?Float, + pickerStyle?: WithDefault<'compact' | 'spinner' | 'inline', 'spinner'>, |}>; type ComponentType = HostComponent; diff --git a/Libraries/Components/DatePickerAndroid/DatePickerAndroid.android.js b/Libraries/Components/DatePickerAndroid/DatePickerAndroid.android.js index 4f9358e21a88ef..9f29433e1e1898 100644 --- a/Libraries/Components/DatePickerAndroid/DatePickerAndroid.android.js +++ b/Libraries/Components/DatePickerAndroid/DatePickerAndroid.android.js @@ -8,8 +8,6 @@ * @flow strict-local */ -'use strict'; - import type {Options, DatePickerOpenAction} from './DatePickerAndroidTypes'; import NativeDatePickerAndroid from './NativeDatePickerAndroid'; diff --git a/Libraries/Components/DatePickerAndroid/NativeDatePickerAndroid.js b/Libraries/Components/DatePickerAndroid/NativeDatePickerAndroid.js index 778befa4ebc84a..c8c157a2f8ecfa 100644 --- a/Libraries/Components/DatePickerAndroid/NativeDatePickerAndroid.js +++ b/Libraries/Components/DatePickerAndroid/NativeDatePickerAndroid.js @@ -8,8 +8,6 @@ * @format */ -'use strict'; - import type {TurboModule} from '../../TurboModule/RCTExport'; import * as TurboModuleRegistry from '../../TurboModule/TurboModuleRegistry'; diff --git a/Libraries/Components/DrawerAndroid/AndroidDrawerLayoutNativeComponent.js b/Libraries/Components/DrawerAndroid/AndroidDrawerLayoutNativeComponent.js index aff340c6f748ab..7efb3491e0e337 100644 --- a/Libraries/Components/DrawerAndroid/AndroidDrawerLayoutNativeComponent.js +++ b/Libraries/Components/DrawerAndroid/AndroidDrawerLayoutNativeComponent.js @@ -8,8 +8,6 @@ * @flow strict-local */ -'use strict'; - import type {ViewProps} from 'react-native/Libraries/Components/View/ViewPropTypes'; import type {ColorValue} from 'react-native/Libraries/StyleSheet/StyleSheet'; import type { diff --git a/Libraries/Components/DrawerAndroid/DrawerLayoutAndroid.android.js b/Libraries/Components/DrawerAndroid/DrawerLayoutAndroid.android.js index 1537250d9b097f..deebd5102e05a3 100644 --- a/Libraries/Components/DrawerAndroid/DrawerLayoutAndroid.android.js +++ b/Libraries/Components/DrawerAndroid/DrawerLayoutAndroid.android.js @@ -8,8 +8,6 @@ * @format */ -'use strict'; - import Platform from '../../Utilities/Platform'; import * as React from 'react'; import StatusBar from '../StatusBar/StatusBar'; diff --git a/Libraries/Components/Keyboard/Keyboard.js b/Libraries/Components/Keyboard/Keyboard.js index a9e0a29771ddfd..e204e00b6c3881 100644 --- a/Libraries/Components/Keyboard/Keyboard.js +++ b/Libraries/Components/Keyboard/Keyboard.js @@ -8,24 +8,14 @@ * @flow strict-local */ -'use strict'; - import NativeEventEmitter from '../../EventEmitter/NativeEventEmitter'; import LayoutAnimation from '../../LayoutAnimation/LayoutAnimation'; import dismissKeyboard from '../../Utilities/dismissKeyboard'; +import Platform from '../../Utilities/Platform'; import NativeKeyboardObserver from './NativeKeyboardObserver'; -import invariant from 'invariant'; -const KeyboardEventEmitter: NativeEventEmitter = new NativeEventEmitter( - NativeKeyboardObserver, -); - -export type KeyboardEventName = - | 'keyboardWillShow' - | 'keyboardDidShow' - | 'keyboardWillHide' - | 'keyboardDidHide' - | 'keyboardWillChangeFrame' - | 'keyboardDidChangeFrame'; +import type {EventSubscription} from '../../vendor/emitter/EventEmitter'; + +export type KeyboardEventName = $Keys; export type KeyboardEventEasing = | 'easeIn' @@ -61,11 +51,14 @@ export type IOSKeyboardEvent = $ReadOnly<{| isEventFromThisApp: boolean, |}>; -type KeyboardEventListener = (e: KeyboardEvent) => void; - -// The following object exists for documentation purposes -// Actual work happens in -// https://github.com/facebook/react-native/blob/master/Libraries/EventEmitter/NativeEventEmitter.js +type KeyboardEventDefinitions = { + keyboardWillShow: [KeyboardEvent], + keyboardDidShow: [KeyboardEvent], + keyboardWillHide: [KeyboardEvent], + keyboardDidHide: [KeyboardEvent], + keyboardWillChangeFrame: [KeyboardEvent], + keyboardDidChangeFrame: [KeyboardEvent], +}; /** * `Keyboard` module to control keyboard events. @@ -109,7 +102,15 @@ type KeyboardEventListener = (e: KeyboardEvent) => void; *``` */ -const Keyboard = { +class Keyboard { + _emitter: NativeEventEmitter = new NativeEventEmitter( + // T88715063: NativeEventEmitter only used this parameter on iOS. Now it uses it on all platforms, so this code was modified automatically to preserve its behavior + // If you want to use the native module on other platforms, please remove this condition and test its behavior + Platform.OS !== 'ios' && Platform.OS !== 'macos' // TODO(macOS GH#774): Also use this parameter on macOS + ? null + : NativeKeyboardObserver, + ); + /** * The `addListener` function connects a JavaScript function to an identified native * keyboard notification event. @@ -133,61 +134,57 @@ const Keyboard = { * * @param {function} callback function to be called when the event fires. */ - addListener(eventName: KeyboardEventName, callback: KeyboardEventListener) { - invariant(false, 'Dummy method used for documentation'); - }, + addListener>( + eventType: K, + listener: (...$ElementType) => mixed, + context?: mixed, + ): EventSubscription { + return this._emitter.addListener(eventType, listener); + } /** - * Removes a specific listener. - * - * @param {string} eventName The `nativeEvent` is the string that identifies the event you're listening for. - * @param {function} callback function to be called when the event fires. + * @deprecated Use `remove` on the EventSubscription from `addEventListener`. */ - removeListener( - eventName: KeyboardEventName, - callback: KeyboardEventListener, - ) { - invariant(false, 'Dummy method used for documentation'); - }, + removeEventListener>( + eventType: K, + listener: (...$ElementType) => mixed, + ): void { + // NOTE: This will report a deprecation notice via `console.error`. + this._emitter.removeListener(eventType, listener); + } /** * Removes all listeners for a specific event type. * * @param {string} eventType The native event string listeners are watching which will be removed. */ - removeAllListeners(eventName: KeyboardEventName) { - invariant(false, 'Dummy method used for documentation'); - }, + removeAllListeners>(eventType: ?K): void { + this._emitter.removeAllListeners(eventType); + } /** * Dismisses the active keyboard and removes focus. */ - dismiss() { - invariant(false, 'Dummy method used for documentation'); - }, + dismiss(): void { + dismissKeyboard(); + } /** * Useful for syncing TextInput (or other keyboard accessory view) size of * position changes with keyboard movements. */ - scheduleLayoutAnimation(event: KeyboardEvent) { - invariant(false, 'Dummy method used for documentation'); - }, -}; - -// Throw away the dummy object and reassign it to original module -KeyboardEventEmitter.dismiss = dismissKeyboard; -KeyboardEventEmitter.scheduleLayoutAnimation = function(event: KeyboardEvent) { - const {duration, easing} = event; - if (duration != null && duration !== 0) { - LayoutAnimation.configureNext({ - duration: duration, - update: { + scheduleLayoutAnimation(event: KeyboardEvent): void { + const {duration, easing} = event; + if (duration != null && duration !== 0) { + LayoutAnimation.configureNext({ duration: duration, - type: (easing != null && LayoutAnimation.Types[easing]) || 'keyboard', - }, - }); + update: { + duration: duration, + type: (easing != null && LayoutAnimation.Types[easing]) || 'keyboard', + }, + }); + } } -}; +} -module.exports = KeyboardEventEmitter; +module.exports = (new Keyboard(): Keyboard); diff --git a/Libraries/Components/Keyboard/KeyboardAvoidingView.js b/Libraries/Components/Keyboard/KeyboardAvoidingView.js index 4e87a3f16c8d76..26897f1632da1e 100644 --- a/Libraries/Components/Keyboard/KeyboardAvoidingView.js +++ b/Libraries/Components/Keyboard/KeyboardAvoidingView.js @@ -5,17 +5,15 @@ * LICENSE file in the root directory of this source tree. * * @format - * @flow + * @flow strict-local */ -'use strict'; - -const Keyboard = require('./Keyboard'); -const LayoutAnimation = require('../../LayoutAnimation/LayoutAnimation'); -const Platform = require('../../Utilities/Platform'); -const React = require('react'); -const StyleSheet = require('../../StyleSheet/StyleSheet'); -const View = require('../View/View'); +import Keyboard from './Keyboard'; +import LayoutAnimation from '../../LayoutAnimation/LayoutAnimation'; +import Platform from '../../Utilities/Platform'; +import * as React from 'react'; +import StyleSheet from '../../StyleSheet/StyleSheet'; +import View from '../View/View'; import type {ViewStyleProp} from '../../StyleSheet/StyleSheet'; import {type EventSubscription} from '../../vendor/emitter/EventEmitter'; @@ -43,13 +41,13 @@ type Props = $ReadOnly<{| * Controls whether this `KeyboardAvoidingView` instance should take effect. * This is useful when more than one is on the screen. Defaults to true. */ - enabled: ?boolean, + enabled?: ?boolean, /** * Distance between the top of the user screen and the React Native view. This * may be non-zero in some cases. Defaults to 0. */ - keyboardVerticalOffset: number, + keyboardVerticalOffset?: number, |}>; type State = {| @@ -61,15 +59,10 @@ type State = {| * adjusting its height, position, or bottom padding. */ class KeyboardAvoidingView extends React.Component { - static defaultProps: {|enabled: boolean, keyboardVerticalOffset: number|} = { - enabled: true, - keyboardVerticalOffset: 0, - }; - _frame: ?ViewLayout = null; _keyboardEvent: ?KeyboardEvent = null; _subscriptions: Array = []; - viewRef: {current: React.ElementRef | null, ...}; + viewRef: {current: React.ElementRef | null, ...}; _initialFrameHeight: number = 0; constructor(props: Props) { @@ -84,7 +77,8 @@ class KeyboardAvoidingView extends React.Component { return 0; } - const keyboardY = keyboardFrame.screenY - this.props.keyboardVerticalOffset; + const keyboardY = + keyboardFrame.screenY - (this.props.keyboardVerticalOffset ?? 0); // Calculate the displacement needed for the view such that it // no longer overlaps with the keyboard @@ -97,13 +91,16 @@ class KeyboardAvoidingView extends React.Component { }; _onLayout = (event: ViewLayoutEvent) => { + const wasFrameNull = this._frame == null; this._frame = event.nativeEvent.layout; if (!this._initialFrameHeight) { // save the initial frame height, before the keyboard is visible this._initialFrameHeight = this._frame.height; } - this._updateBottomIfNecesarry(); + if (wasFrameNull) { + this._updateBottomIfNecesarry(); + } }; _updateBottomIfNecesarry = () => { @@ -156,12 +153,13 @@ class KeyboardAvoidingView extends React.Component { behavior, children, contentContainerStyle, - enabled, - keyboardVerticalOffset, + enabled = true, + // eslint-disable-next-line no-unused-vars + keyboardVerticalOffset = 0, style, ...props } = this.props; - const bottomHeight = enabled ? this.state.bottom : 0; + const bottomHeight = enabled === true ? this.state.bottom : 0; switch (behavior) { case 'height': let heightStyle; @@ -226,4 +224,4 @@ class KeyboardAvoidingView extends React.Component { } } -module.exports = KeyboardAvoidingView; +export default KeyboardAvoidingView; diff --git a/Libraries/Components/Keyboard/NativeKeyboardObserver.js b/Libraries/Components/Keyboard/NativeKeyboardObserver.js index 7f077c3c88baa8..7868dd9fb0941a 100644 --- a/Libraries/Components/Keyboard/NativeKeyboardObserver.js +++ b/Libraries/Components/Keyboard/NativeKeyboardObserver.js @@ -8,8 +8,6 @@ * @format */ -'use strict'; - import type {TurboModule} from '../../TurboModule/RCTExport'; import * as TurboModuleRegistry from '../../TurboModule/TurboModuleRegistry'; diff --git a/Libraries/Components/Keyboard/__tests__/Keyboard-test.js b/Libraries/Components/Keyboard/__tests__/Keyboard-test.js index 639cdd0db5f4f0..2c7befec418355 100644 --- a/Libraries/Components/Keyboard/__tests__/Keyboard-test.js +++ b/Libraries/Components/Keyboard/__tests__/Keyboard-test.js @@ -9,41 +9,27 @@ * @emails oncall+react_native */ -'use strict'; - -const NativeModules = require('../../../BatchedBridge/NativeModules'); const LayoutAnimation = require('../../../LayoutAnimation/LayoutAnimation'); const dismissKeyboard = require('../../../Utilities/dismissKeyboard'); const Keyboard = require('../Keyboard'); -import NativeEventEmitter from '../../../EventEmitter/NativeEventEmitter'; - jest.mock('../../../LayoutAnimation/LayoutAnimation'); +jest.mock('../../../Utilities/dismissKeyboard'); describe('Keyboard', () => { beforeEach(() => { jest.resetAllMocks(); }); - it('exposes KeyboardEventEmitter methods', () => { - const KeyboardObserver = NativeModules.KeyboardObserver; - const KeyboardEventEmitter = new NativeEventEmitter(KeyboardObserver); - - // $FlowFixMe - expect(Keyboard._subscriber).toBe(KeyboardEventEmitter._subscriber); - // $FlowFixMe Cannot access private property - expect(Keyboard._nativeModule).toBe(KeyboardEventEmitter._nativeModule); - }); - it('uses dismissKeyboard utility', () => { - expect(Keyboard.dismiss).toBe(dismissKeyboard); + Keyboard.dismiss(); + expect(dismissKeyboard).toHaveBeenCalled(); }); describe('scheduling layout animation', () => { - const scheduleLayoutAnimation = ( - duration: number | null, - easing: string | null, - ): void => Keyboard.scheduleLayoutAnimation({duration, easing}); + const scheduleLayoutAnimation = (duration, easing): void => + // $FlowFixMe[incompatible-call] + Keyboard.scheduleLayoutAnimation({duration, easing}); it('triggers layout animation', () => { scheduleLayoutAnimation(12, 'spring'); diff --git a/Libraries/Components/MaskedView/MaskedViewIOS.ios.js b/Libraries/Components/MaskedView/MaskedViewIOS.ios.js index 4a32d29836f8d0..eb69fc4233affc 100644 --- a/Libraries/Components/MaskedView/MaskedViewIOS.ios.js +++ b/Libraries/Components/MaskedView/MaskedViewIOS.ios.js @@ -8,9 +8,9 @@ * @flow */ -const React = require('react'); -const StyleSheet = require('../../StyleSheet/StyleSheet'); -const View = require('../View/View'); +import * as React from 'react'; +import StyleSheet from '../../StyleSheet/StyleSheet'; +import View from '../View/View'; import type {ViewProps} from '../View/ViewPropTypes'; import RCTMaskedViewNativeComponent from './RCTMaskedViewNativeComponent'; diff --git a/Libraries/Components/Picker/AndroidDialogPickerNativeComponent.js b/Libraries/Components/Picker/AndroidDialogPickerNativeComponent.js deleted file mode 100644 index d01757865909fc..00000000000000 --- a/Libraries/Components/Picker/AndroidDialogPickerNativeComponent.js +++ /dev/null @@ -1,82 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its 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 - */ - -'use strict'; - -import * as React from 'react'; - -import codegenNativeCommands from '../../Utilities/codegenNativeCommands'; -import requireNativeComponent from '../../ReactNative/requireNativeComponent'; -import registerGeneratedViewConfig from '../../Utilities/registerGeneratedViewConfig'; -import AndroidDialogPickerViewConfig from './AndroidDialogPickerViewConfig'; - -import type { - DirectEventHandler, - Int32, - WithDefault, -} from '../../Types/CodegenTypes'; -import type {HostComponent} from '../../Renderer/shims/ReactNativeTypes'; -import type {TextStyleProp} from '../../StyleSheet/StyleSheet'; -import type {ColorValue} from '../../StyleSheet/StyleSheet'; -import type {ProcessedColorValue} from '../../StyleSheet/processColor'; -import type {ViewProps} from '../../Components/View/ViewPropTypes'; - -type PickerItem = $ReadOnly<{| - label: string, - color?: ?ProcessedColorValue, -|}>; - -type PickerItemSelectEvent = $ReadOnly<{| - position: Int32, -|}>; - -type NativeProps = $ReadOnly<{| - ...ViewProps, - style?: ?TextStyleProp, - - // Props - color?: ?ColorValue, - backgroundColor?: ?ColorValue, - enabled?: WithDefault, - items: $ReadOnlyArray, - prompt?: WithDefault, - selected: Int32, - - // Events - onSelect?: DirectEventHandler, -|}>; - -type NativeType = HostComponent; - -interface NativeCommands { - +setNativeSelectedPosition: ( - viewRef: React.ElementRef, - index: number, - ) => void; -} - -export const Commands: NativeCommands = codegenNativeCommands({ - supportedCommands: ['setNativeSelectedPosition'], -}); - -let AndroidDialogPickerNativeComponent; -if (global.RN$Bridgeless) { - registerGeneratedViewConfig( - 'AndroidDialogPicker', - AndroidDialogPickerViewConfig, - ); - AndroidDialogPickerNativeComponent = 'AndroidDialogPicker'; -} else { - AndroidDialogPickerNativeComponent = requireNativeComponent( - 'AndroidDialogPicker', - ); -} - -export default ((AndroidDialogPickerNativeComponent: any): NativeType); diff --git a/Libraries/Components/Picker/AndroidDialogPickerViewConfig.js b/Libraries/Components/Picker/AndroidDialogPickerViewConfig.js deleted file mode 100644 index 1108b8105e37e1..00000000000000 --- a/Libraries/Components/Picker/AndroidDialogPickerViewConfig.js +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @flow strict-local - * @format - */ - -'use strict'; - -import type {GeneratedViewConfig} from '../../Utilities/registerGeneratedViewConfig'; - -const AndroidDialogPickerViewConfig = { - uiViewClassName: 'AndroidDialogPicker', - bubblingEventTypes: {}, - directEventTypes: {}, - validAttributes: { - color: {process: require('../../StyleSheet/processColor')}, - backgroundColor: {process: require('../../StyleSheet/processColor')}, - enabled: true, - items: true, - prompt: true, - selected: true, - onSelect: true, - }, -}; - -module.exports = (AndroidDialogPickerViewConfig: GeneratedViewConfig); diff --git a/Libraries/Components/Picker/AndroidDropdownPickerNativeComponent.js b/Libraries/Components/Picker/AndroidDropdownPickerNativeComponent.js deleted file mode 100644 index f6ddb741888247..00000000000000 --- a/Libraries/Components/Picker/AndroidDropdownPickerNativeComponent.js +++ /dev/null @@ -1,69 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its 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 strict'; - -import * as React from 'react'; - -import codegenNativeCommands from '../../Utilities/codegenNativeCommands'; -import requireNativeComponent from '../../ReactNative/requireNativeComponent'; - -import type { - DirectEventHandler, - Int32, - WithDefault, -} from '../../Types/CodegenTypes'; -import type {HostComponent} from '../../Renderer/shims/ReactNativeTypes'; -import type {TextStyleProp} from '../../StyleSheet/StyleSheet'; -import type {ColorValue} from '../../StyleSheet/StyleSheet'; -import type {ProcessedColorValue} from '../../StyleSheet/processColor'; -import type {ViewProps} from '../../Components/View/ViewPropTypes'; - -type PickerItem = $ReadOnly<{| - label: string, - color?: ?ProcessedColorValue, -|}>; - -type PickerItemSelectEvent = $ReadOnly<{| - position: Int32, -|}>; - -type NativeProps = $ReadOnly<{| - ...ViewProps, - style?: ?TextStyleProp, - - // Props - color?: ?ColorValue, - backgroundColor?: ?ColorValue, - enabled?: WithDefault, - items: $ReadOnlyArray, - prompt?: WithDefault, - selected: Int32, - - // Events - onSelect?: DirectEventHandler, -|}>; - -type NativeType = HostComponent; - -interface NativeCommands { - +setNativeSelectedPosition: ( - viewRef: React.ElementRef, - index: number, - ) => void; -} - -export const Commands: NativeCommands = codegenNativeCommands({ - supportedCommands: ['setNativeSelectedPosition'], -}); - -export default (requireNativeComponent( - 'AndroidDropdownPicker', -): NativeType); diff --git a/Libraries/Components/Picker/Picker.js b/Libraries/Components/Picker/Picker.js deleted file mode 100644 index 01e5ae3664862d..00000000000000 --- a/Libraries/Components/Picker/Picker.js +++ /dev/null @@ -1,168 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its 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 - */ - -'use strict'; - -const PickerAndroid = require('./PickerAndroid'); -const PickerIOS = require('./PickerIOS'); -const Platform = require('../../Utilities/Platform'); -const React = require('react'); -const UnimplementedView = require('../UnimplementedViews/UnimplementedView'); - -import type {TextStyleProp} from '../../StyleSheet/StyleSheet'; -import type {ColorValue} from '../../StyleSheet/StyleSheet'; - -const MODE_DIALOG = 'dialog'; -const MODE_DROPDOWN = 'dropdown'; - -type PickerItemProps = $ReadOnly<{| - /** - * Text to display for this item. - */ - label: string, - - /** - * The value to be passed to picker's `onValueChange` callback when - * this item is selected. - */ - value?: ?string, - - /** - * Color of this item's text. - * @platform android - */ - color?: ColorValue, - - /** - * Used to locate the item in end-to-end tests. - */ - testID?: string, -|}>; - -/** - * Individual selectable item in a Picker. - */ -export type {PickerItem}; -class PickerItem extends React.Component { - render() { - // The items are not rendered directly - throw null; - } -} - -type PickerProps = $ReadOnly<{| - children?: React.Node, - style?: ?TextStyleProp, - - /** - * Value matching value of one of the items. - */ - selectedValue?: ?string, - - /** - * Callback for when an item is selected. This is called with the following parameters: - * - `itemValue`: the `value` prop of the item that was selected - * - `itemIndex`: the index of the selected item in this picker - */ - onValueChange?: ?(itemValue: string | number, itemIndex: number) => mixed, - - /** - * If set to false, the picker will be disabled, i.e. the user will not be able to make a - * selection. - * @platform android - */ - enabled?: ?boolean, - - /** - * On Android, specifies how to display the selection items when the user taps on the picker: - * - * - 'dialog': Show a modal dialog. This is the default. - * - 'dropdown': Shows a dropdown anchored to the picker view - * - * @platform android - */ - mode?: ?('dialog' | 'dropdown'), - - /** - * Style to apply to each of the item labels. - * @platform ios - */ - itemStyle?: ?TextStyleProp, - - /** - * Color of the item background. - * @platform android - */ - backgroundColor?: ColorValue, - - /** - * Prompt string for this picker, used on Android in dialog mode as the title of the dialog. - * @platform android - */ - prompt?: ?string, - - /** - * Used to locate this view in end-to-end tests. - */ - testID?: ?string, - /** - * The string used for the accessibility label. Will be read once focused on the picker but not on change. - */ - accessibilityLabel?: ?string, -|}>; - -/** - * Renders the native picker component on iOS and Android. Example: - * - * this.setState({language: itemValue})}> - * - * - * - */ -class Picker extends React.Component { - /** - * On Android, display the options in a dialog. - */ - static MODE_DIALOG: $TEMPORARY$string<'dialog'> = MODE_DIALOG; - - /** - * On Android, display the options in a dropdown (this is the default). - */ - static MODE_DROPDOWN: $TEMPORARY$string<'dropdown'> = MODE_DROPDOWN; - - static Item: typeof PickerItem = PickerItem; - - static defaultProps: {|mode: $TEMPORARY$string<'dialog'>|} = { - mode: MODE_DIALOG, - }; - - render(): React.Node { - if ( - Platform.OS === 'ios' || - Platform.OS === 'macos' /* TODO(macOS GH#774) */ - ) { - /* $FlowFixMe(>=0.81.0 site=react_native_ios_fb) This suppression was - * added when renaming suppression sites. */ - return {this.props.children}; - } else if (Platform.OS === 'android') { - return ( - /* $FlowFixMe(>=0.81.0 site=react_native_android_fb) This suppression - * was added when renaming suppression sites. */ - {this.props.children} - ); - } else { - return ; - } - } -} - -module.exports = Picker; diff --git a/Libraries/Components/Picker/PickerAndroid.android.js b/Libraries/Components/Picker/PickerAndroid.android.js deleted file mode 100644 index ccefd18833d214..00000000000000 --- a/Libraries/Components/Picker/PickerAndroid.android.js +++ /dev/null @@ -1,145 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its 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 strict'; - -import AndroidDropdownPickerNativeComponent, { - Commands as AndroidDropdownPickerCommands, -} from './AndroidDropdownPickerNativeComponent'; -import AndroidDialogPickerNativeComponent, { - Commands as AndroidDialogPickerCommands, -} from './AndroidDialogPickerNativeComponent'; -import * as React from 'react'; -import StyleSheet from '../../StyleSheet/StyleSheet'; -import invariant from 'invariant'; -import processColor from '../../StyleSheet/processColor'; - -import type {SyntheticEvent} from '../../Types/CoreEventTypes'; -import type {ColorValue, TextStyleProp} from '../../StyleSheet/StyleSheet'; - -type PickerItemSelectSyntheticEvent = SyntheticEvent< - $ReadOnly<{| - position: number, - |}>, ->; - -type PickerItemValue = number | string; - -type Props = $ReadOnly<{| - accessibilityLabel?: ?Stringish, - children?: React.Node, - style?: ?TextStyleProp, - backgroundColor?: ?ColorValue, - selectedValue?: ?PickerItemValue, - enabled?: ?boolean, - mode?: ?('dialog' | 'dropdown'), - onValueChange?: ?(itemValue: ?PickerItemValue, itemIndex: number) => mixed, - prompt?: ?string, - testID?: string, -|}>; - -/** - * Not exposed as a public API - use instead. - */ -function PickerAndroid(props: Props): React.Node { - const pickerRef = React.useRef(null); - - const [items, selected] = React.useMemo(() => { - // eslint-disable-next-line no-shadow - let selected = 0; - // eslint-disable-next-line no-shadow - const items = React.Children.map(props.children, (child, index) => { - if (child === null) { - return null; - } - if (child.props.value === props.selectedValue) { - selected = index; - } - const {color, label} = child.props; - const processedColor = processColor(color); - invariant( - processedColor == null || typeof processedColor === 'number', - 'Unexpected color given for PickerAndroid color prop', - ); - return { - color: color == null ? null : processedColor, - label, - }; - }); - return [items, selected]; - }, [props.children, props.selectedValue]); - - const onSelect = React.useCallback( - ({nativeEvent}: PickerItemSelectSyntheticEvent) => { - const {position} = nativeEvent; - const onValueChange = props.onValueChange; - - if (onValueChange != null) { - if (position >= 0) { - const children = React.Children.toArray(props.children).filter( - item => item != null, - ); - const value = children[position].props.value; - if (props.selectedValue !== value) { - onValueChange(value, position); - } - } else { - onValueChange(null, position); - } - } - const {current} = pickerRef; - if (current != null && position !== selected) { - const Commands = - props.mode === 'dropdown' - ? AndroidDropdownPickerCommands - : AndroidDialogPickerCommands; - Commands.setNativeSelectedPosition(current, position); - } - }, - [ - props.children, - props.onValueChange, - props.selectedValue, - props.mode, - selected, - ], - ); - - const rootProps = { - accessibilityLabel: props.accessibilityLabel, - enabled: props.enabled, - items, - onSelect, - prompt: props.prompt, - ref: pickerRef, - selected, - style: StyleSheet.compose(styles.pickerAndroid, props.style), - backgroundColor: props.backgroundColor, - testID: props.testID, - }; - return props.mode === 'dropdown' ? ( - - ) : ( - - ); -} - -const styles = StyleSheet.create({ - pickerAndroid: { - // The picker will conform to whatever width is given, but we do - // have to set the component's height explicitly on the - // surrounding view to ensure it gets rendered. - // TODO would be better to export a native constant for this, - // like in iOS the RCTDatePickerManager.m - height: 50, - }, -}); - -module.exports = PickerAndroid; diff --git a/Libraries/Components/Picker/PickerIOS.ios.js b/Libraries/Components/Picker/PickerIOS.ios.js deleted file mode 100644 index 4b4c50d00b1d2a..00000000000000 --- a/Libraries/Components/Picker/PickerIOS.ios.js +++ /dev/null @@ -1,165 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its 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 - */ - -// This is a controlled component version of RCTPickerIOS. - -'use strict'; - -const React = require('react'); -const StyleSheet = require('../../StyleSheet/StyleSheet'); -const View = require('../View/View'); -const Platform = require('../../Utilities/Platform'); // TODO(macOS GH#774) - -const invariant = require('invariant'); -const processColor = require('../../StyleSheet/processColor'); - -import RCTPickerNativeComponent, { - Commands as PickerCommands, -} from './RCTPickerNativeComponent'; -import type {TextStyleProp} from '../../StyleSheet/StyleSheet'; -import type {ColorValue} from '../../StyleSheet/StyleSheet'; -import type {ProcessedColorValue} from '../../StyleSheet/processColor'; -import type {SyntheticEvent} from '../../Types/CoreEventTypes'; -import type {ViewProps} from '../View/ViewPropTypes'; - -type PickerIOSChangeEvent = SyntheticEvent< - $ReadOnly<{| - newValue: number | string, - newIndex: number, - |}>, ->; - -type RCTPickerIOSItemType = $ReadOnly<{| - label: ?Label, - value: ?string, - textColor: ?ProcessedColorValue, -|}>; - -type Label = Stringish | number; - -type Props = $ReadOnly<{| - ...ViewProps, - children: React.ChildrenArray>, - itemStyle?: ?TextStyleProp, - onChange?: ?(event: PickerIOSChangeEvent) => mixed, - onValueChange?: ?(itemValue: string | number, itemIndex: number) => mixed, - selectedValue: ?string, - accessibilityLabel?: ?string, -|}>; - -type State = {| - selectedIndex: number, - items: $ReadOnlyArray, -|}; - -type ItemProps = $ReadOnly<{| - label: ?Label, - value?: ?string, - color?: ?ColorValue, -|}>; - -const PickerIOSItem = (props: ItemProps): null => { - return null; -}; - -class PickerIOS extends React.Component { - _picker: ?React.ElementRef = null; - _lastNativeValue: ?number; - - state: State = { - selectedIndex: 0, - items: [], - }; - - static Item: (props: ItemProps) => null = PickerIOSItem; - - static getDerivedStateFromProps(props: Props): State { - let selectedIndex = 0; - const items = []; - React.Children.toArray(props.children) - .filter(child => child !== null) - .forEach(function(child, index) { - if (child.props.value === props.selectedValue) { - selectedIndex = index; - } - const processedTextColor = processColor(child.props.color); - invariant( - processedTextColor == null || typeof processedTextColor === 'number', - 'Unexpected color given for PickerIOSItem color', - ); - items.push({ - value: child.props.value, - label: child.props.label, - textColor: processedTextColor, - }); - }); - return {selectedIndex, items}; - } - - render(): React.Node { - return ( - - { - this._picker = picker; - }} - testID={this.props.testID} - style={[styles.pickerIOS, this.props.itemStyle]} - items={this.state.items} - selectedIndex={this.state.selectedIndex} - onChange={this._onChange} - accessibilityLabel={this.props.accessibilityLabel} - /> - - ); - } - - componentDidUpdate() { - // This is necessary in case native updates the picker and JS decides - // that the update should be ignored and we should stick with the value - // that we have in JS. - if ( - this._picker && - this._lastNativeValue !== undefined && - this._lastNativeValue !== this.state.selectedIndex - ) { - PickerCommands.setNativeSelectedIndex( - this._picker, - this.state.selectedIndex, - ); - } - } - - _onChange = event => { - if (this.props.onChange) { - this.props.onChange(event); - } - if (this.props.onValueChange) { - this.props.onValueChange( - event.nativeEvent.newValue, - event.nativeEvent.newIndex, - ); - } - - this._lastNativeValue = event.nativeEvent.newIndex; - this.forceUpdate(); - }; -} - -const styles = StyleSheet.create({ - pickerIOS: { - // The picker will conform to whatever width is given, but we do - // have to set the component's height explicitly on the - // surrounding view to ensure it gets rendered. - height: Platform.OS === 'ios' ? 216 : 25, // TODO(macOS GH#774): NSComboBox in Storyboards is 25 points. - }, -}); - -module.exports = PickerIOS; diff --git a/Libraries/Components/Picker/RCTPickerNativeComponent.js b/Libraries/Components/Picker/RCTPickerNativeComponent.js deleted file mode 100644 index 7fdbe9c76c80fd..00000000000000 --- a/Libraries/Components/Picker/RCTPickerNativeComponent.js +++ /dev/null @@ -1,72 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @flow strict-local - * @format - */ - -'use strict'; - -const requireNativeComponent = require('../../ReactNative/requireNativeComponent'); -const ReactNativeViewConfigRegistry = require('../../Renderer/shims/ReactNativeViewConfigRegistry'); - -import type {HostComponent} from '../../Renderer/shims/ReactNativeTypes'; -import type {SyntheticEvent} from '../../Types/CoreEventTypes'; -import type {TextStyleProp} from '../../StyleSheet/StyleSheet'; -import type {ProcessedColorValue} from '../../StyleSheet/processColor'; -import codegenNativeCommands from '../../Utilities/codegenNativeCommands'; -import RCTPickerViewConfig from './RCTPickerViewConfig'; -import * as React from 'react'; - -type PickerIOSChangeEvent = SyntheticEvent< - $ReadOnly<{| - newValue: number | string, - newIndex: number, - |}>, ->; - -type RCTPickerIOSItemType = $ReadOnly<{| - label: ?Label, - value: ?string, - textColor: ?ProcessedColorValue, -|}>; - -type Label = Stringish | number; - -type NativeProps = $ReadOnly<{| - items: $ReadOnlyArray, - onChange: (event: PickerIOSChangeEvent) => void, - selectedIndex: number, - style?: ?TextStyleProp, - testID?: ?string, - accessibilityLabel?: ?string, -|}>; - -type ComponentType = HostComponent; - -interface NativeCommands { - +setNativeSelectedIndex: ( - viewRef: React.ElementRef, - index: number, - ) => void; -} - -export const Commands: NativeCommands = codegenNativeCommands({ - supportedCommands: ['setNativeSelectedIndex'], -}); - -let RCTPickerNativeComponent; -if (global.RN$Bridgeless) { - ReactNativeViewConfigRegistry.register('RCTPicker', () => { - return RCTPickerViewConfig; - }); - RCTPickerNativeComponent = 'RCTPicker'; -} else { - RCTPickerNativeComponent = requireNativeComponent('RCTPicker'); -} - -// flowlint-next-line unclear-type:off -export default ((RCTPickerNativeComponent: any): HostComponent); diff --git a/Libraries/Components/Picker/RCTPickerViewConfig.js b/Libraries/Components/Picker/RCTPickerViewConfig.js deleted file mode 100644 index 448bc2a2b8e5cf..00000000000000 --- a/Libraries/Components/Picker/RCTPickerViewConfig.js +++ /dev/null @@ -1,41 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @flow strict-local - * @format - */ - -'use strict'; - -import ReactNativeViewViewConfig from '../../Components/View/ReactNativeViewViewConfig'; -import type {ReactNativeBaseComponentViewConfig} from '../../Renderer/shims/ReactNativeTypes'; - -const RCTPickerViewConfig = { - uiViewClassName: 'RCTPicker', - bubblingEventTypes: { - topChange: { - phasedRegistrationNames: { - bubbled: 'onChange', - captured: 'onChangeCapture', - }, - }, - }, - directEventTypes: {}, - validAttributes: { - ...ReactNativeViewViewConfig.validAttributes, - color: {process: require('../../StyleSheet/processColor')}, - fontFamily: true, - fontSize: true, - fontStyle: true, - fontWeight: true, - items: true, - onChange: true, - selectedIndex: true, - textAlign: true, - }, -}; - -module.exports = (RCTPickerViewConfig: ReactNativeBaseComponentViewConfig<>); diff --git a/Libraries/Components/Picker/__tests__/Picker-test.js b/Libraries/Components/Picker/__tests__/Picker-test.js deleted file mode 100644 index 1a22421c5686c0..00000000000000 --- a/Libraries/Components/Picker/__tests__/Picker-test.js +++ /dev/null @@ -1,34 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @format - * @emails oncall+react_native - * @flow strict-local - */ - -'use strict'; - -const React = require('react'); -const Picker = require('../Picker'); - -const ReactNativeTestTools = require('../../../Utilities/ReactNativeTestTools'); - -describe('', () => { - it('should render as expected', () => { - ReactNativeTestTools.expectRendersMatchingSnapshot( - 'Picker', - () => ( - - - - - ), - () => { - jest.dontMock('../Picker'); - }, - ); - }); -}); diff --git a/Libraries/Components/Picker/__tests__/__snapshots__/Picker-test.js.snap b/Libraries/Components/Picker/__tests__/__snapshots__/Picker-test.js.snap deleted file mode 100644 index 32c40aa992d57a..00000000000000 --- a/Libraries/Components/Picker/__tests__/__snapshots__/Picker-test.js.snap +++ /dev/null @@ -1,97 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` should render as expected: should deep render when mocked (please verify output manually) 1`] = ` - - - -`; - -exports[` should render as expected: should deep render when not mocked (please verify output manually) 1`] = ` - - - -`; - -exports[` should render as expected: should shallow render as when mocked 1`] = ` - - - - -`; - -exports[` should render as expected: should shallow render as when not mocked 1`] = ` - - - - -`; diff --git a/Libraries/Components/Pressable/Pressable.js b/Libraries/Components/Pressable/Pressable.js index ec0e2343dfb3ee..59760863a020ee 100644 --- a/Libraries/Components/Pressable/Pressable.js +++ b/Libraries/Components/Pressable/Pressable.js @@ -8,8 +8,6 @@ * @format */ -'use strict'; - import * as React from 'react'; import {useMemo, useState, useRef, useImperativeHandle} from 'react'; import useAndroidRippleForView, { @@ -58,6 +56,12 @@ type Props = $ReadOnly<{| importantForAccessibility?: ?('auto' | 'yes' | 'no' | 'no-hide-descendants'), onAccessibilityAction?: ?(event: AccessibilityActionEvent) => mixed, + /** + * Whether a press gesture can be interrupted by a parent gesture such as a + * scroll event. Defaults to true. + */ + cancelable?: ?boolean, + /** * Either children or a render prop that receives a boolean reflecting whether * the component is currently pressed. @@ -98,7 +102,7 @@ type Props = $ReadOnly<{| /** * Called when this view's layout changes. */ - onLayout?: ?(event: LayoutEvent) => void, + onLayout?: ?(event: LayoutEvent) => mixed, /** * Called when the hover is activated to provide visual feedback. @@ -113,22 +117,22 @@ type Props = $ReadOnly<{| /** * Called when a long-tap gesture is detected. */ - onLongPress?: ?(event: PressEvent) => void, + onLongPress?: ?(event: PressEvent) => mixed, /** * Called when a single tap gesture is detected. */ - onPress?: ?(event: PressEvent) => void, + onPress?: ?(event: PressEvent) => mixed, /** * Called when a touch is engaged before `onPress`. */ - onPressIn?: ?(event: PressEvent) => void, + onPressIn?: ?(event: PressEvent) => mixed, /** * Called when a touch is released before `onPress`. */ - onPressOut?: ?(event: PressEvent) => void, + onPressOut?: ?(event: PressEvent) => mixed, /** * Either view styles or a function that receives a boolean reflecting whether @@ -183,6 +187,7 @@ function Pressable(props: Props, forwardedRef): React.Node { accessible, android_disableSound, android_ripple, + cancelable, children, delayHoverIn, delayHoverOut, @@ -211,6 +216,11 @@ function Pressable(props: Props, forwardedRef): React.Node { const hitSlop = normalizeRect(props.hitSlop); + const accessibilityState = + disabled != null + ? {...props.accessibilityState, disabled} + : props.accessibilityState; + const restPropsWithDefaults: React.ElementConfig = { ...restProps, ...android_rippleConfig?.viewProps, @@ -218,11 +228,13 @@ function Pressable(props: Props, forwardedRef): React.Node { enableFocusRing: enableFocusRing !== false && !disabled, accessible: accessible !== false, focusable: focusable !== false && !disabled, // ]TODO(macOS GH#774) + accessibilityState, hitSlop, }; const config = useMemo( () => ({ + cancelable, disabled, hitSlop, pressRectOffset: pressRetentionOffset, @@ -258,6 +270,7 @@ function Pressable(props: Props, forwardedRef): React.Node { [ android_disableSound, android_rippleConfig, + cancelable, delayHoverIn, delayHoverOut, delayLongPress, diff --git a/Libraries/Components/Pressable/__tests__/Pressable-test.js b/Libraries/Components/Pressable/__tests__/Pressable-test.js index a8c5af535e8518..46909623347aa1 100644 --- a/Libraries/Components/Pressable/__tests__/Pressable-test.js +++ b/Libraries/Components/Pressable/__tests__/Pressable-test.js @@ -9,8 +9,6 @@ * @flow strict-local */ -'use strict'; - import * as React from 'react'; import Pressable from '../Pressable'; @@ -32,3 +30,67 @@ describe('', () => { ); }); }); + +describe('', () => { + it('should be disabled when disabled is true', () => { + expectRendersMatchingSnapshot( + 'Pressable', + () => ( + + + + ), + () => { + jest.dontMock('../Pressable'); + }, + ); + }); +}); + +describe('', () => { + it('should be disabled when disabled is true and accessibilityState is empty', () => { + expectRendersMatchingSnapshot( + 'Pressable', + () => ( + + + + ), + () => { + jest.dontMock('../Pressable'); + }, + ); + }); +}); + +describe('', () => { + it('should keep accessibilityState when disabled is true', () => { + expectRendersMatchingSnapshot( + 'Pressable', + () => ( + + + + ), + () => { + jest.dontMock('../Pressable'); + }, + ); + }); +}); + +describe('', () => { + it('should overwrite accessibilityState with value of disabled prop', () => { + expectRendersMatchingSnapshot( + 'Pressable', + () => ( + + + + ), + () => { + jest.dontMock('../Pressable'); + }, + ); + }); +}); diff --git a/Libraries/Components/Pressable/__tests__/__snapshots__/Pressable-test.js.snap b/Libraries/Components/Pressable/__tests__/__snapshots__/Pressable-test.js.snap index 6f244c492b92fb..c4add53c23e111 100644 --- a/Libraries/Components/Pressable/__tests__/__snapshots__/Pressable-test.js.snap +++ b/Libraries/Components/Pressable/__tests__/__snapshots__/Pressable-test.js.snap @@ -57,3 +57,315 @@ exports[` should render as expected: should shallow render as `; + +exports[` should be disabled when disabled is true: should deep render when mocked (please verify output manually) 1`] = ` + + + +`; + +exports[` should be disabled when disabled is true: should deep render when not mocked (please verify output manually) 1`] = ` + + + +`; + +exports[` should be disabled when disabled is true: should shallow render as when mocked 1`] = ` + + + +`; + +exports[` should be disabled when disabled is true: should shallow render as when not mocked 1`] = ` + + + +`; + +exports[` should be disabled when disabled is true and accessibilityState is empty: should deep render when mocked (please verify output manually) 1`] = ` + + + +`; + +exports[` should be disabled when disabled is true and accessibilityState is empty: should deep render when not mocked (please verify output manually) 1`] = ` + + + +`; + +exports[` should be disabled when disabled is true and accessibilityState is empty: should shallow render as when mocked 1`] = ` + + + +`; + +exports[` should be disabled when disabled is true and accessibilityState is empty: should shallow render as when not mocked 1`] = ` + + + +`; + +exports[` should keep accessibilityState when disabled is true: should deep render when mocked (please verify output manually) 1`] = ` + + + +`; + +exports[` should keep accessibilityState when disabled is true: should deep render when not mocked (please verify output manually) 1`] = ` + + + +`; + +exports[` should keep accessibilityState when disabled is true: should shallow render as when mocked 1`] = ` + + + +`; + +exports[` should keep accessibilityState when disabled is true: should shallow render as when not mocked 1`] = ` + + + +`; + +exports[` should overwrite accessibilityState with value of disabled prop: should deep render when mocked (please verify output manually) 1`] = ` + + + +`; + +exports[` should overwrite accessibilityState with value of disabled prop: should deep render when not mocked (please verify output manually) 1`] = ` + + + +`; + +exports[` should overwrite accessibilityState with value of disabled prop: should shallow render as when mocked 1`] = ` + + + +`; + +exports[` should overwrite accessibilityState with value of disabled prop: should shallow render as when not mocked 1`] = ` + + + +`; diff --git a/Libraries/Components/Pressable/useAndroidRippleForView.js b/Libraries/Components/Pressable/useAndroidRippleForView.js index 1e33f2a144c762..18ee3d5c6a4623 100644 --- a/Libraries/Components/Pressable/useAndroidRippleForView.js +++ b/Libraries/Components/Pressable/useAndroidRippleForView.js @@ -8,8 +8,6 @@ * @format */ -'use strict'; - import invariant from 'invariant'; import {Commands} from '../View/ViewNativeComponent'; import type {ColorValue} from '../../StyleSheet/StyleSheet'; @@ -29,6 +27,7 @@ export type RippleConfig = {| color?: ColorValue, borderless?: boolean, radius?: number, + foreground?: boolean, |}; /** @@ -42,11 +41,11 @@ export default function useAndroidRippleForView( onPressIn: (event: PressEvent) => void, onPressMove: (event: PressEvent) => void, onPressOut: (event: PressEvent) => void, - viewProps: $ReadOnly<{| - nativeBackgroundAndroid: NativeBackgroundProp, - |}>, + viewProps: + | $ReadOnly<{|nativeBackgroundAndroid: NativeBackgroundProp|}> + | $ReadOnly<{|nativeForegroundAndroid: NativeBackgroundProp|}>, |}> { - const {color, borderless, radius} = rippleConfig ?? {}; + const {color, borderless, radius, foreground} = rippleConfig ?? {}; return useMemo(() => { if ( @@ -60,25 +59,27 @@ export default function useAndroidRippleForView( 'Unexpected color given for Ripple color', ); + const nativeRippleValue = { + type: 'RippleAndroid', + color: processedColor, + borderless: borderless === true, + rippleRadius: radius, + }; + return { - viewProps: { - // Consider supporting `nativeForegroundAndroid` - nativeBackgroundAndroid: { - type: 'RippleAndroid', - color: processedColor, - borderless: borderless === true, - rippleRadius: radius, - }, - }, + viewProps: + foreground === true + ? {nativeForegroundAndroid: nativeRippleValue} + : {nativeBackgroundAndroid: nativeRippleValue}, onPressIn(event: PressEvent): void { const view = viewRef.current; if (view != null) { - Commands.setPressed(view, true); Commands.hotspotUpdate( view, event.nativeEvent.locationX ?? 0, event.nativeEvent.locationY ?? 0, ); + Commands.setPressed(view, true); } }, onPressMove(event: PressEvent): void { @@ -100,5 +101,5 @@ export default function useAndroidRippleForView( }; } return null; - }, [color, borderless, radius, viewRef]); + }, [borderless, color, foreground, radius, viewRef]); } diff --git a/Libraries/Components/ProgressBarAndroid/ProgressBarAndroid.android.js b/Libraries/Components/ProgressBarAndroid/ProgressBarAndroid.android.js index 61663d6ad50b30..51803f1df2e8a6 100644 --- a/Libraries/Components/ProgressBarAndroid/ProgressBarAndroid.android.js +++ b/Libraries/Components/ProgressBarAndroid/ProgressBarAndroid.android.js @@ -8,8 +8,6 @@ * @format */ -'use strict'; - const React = require('react'); import ProgressBarAndroidNativeComponent from './ProgressBarAndroidNativeComponent'; @@ -81,23 +79,27 @@ export type ProgressBarAndroidProps = $ReadOnly<{| * ``` */ const ProgressBarAndroid = ( - props: ProgressBarAndroidProps, + { + styleAttr = 'Normal', + indeterminate = true, + animating = true, + ...restProps + }: ProgressBarAndroidProps, forwardedRef: ?React.Ref, ) => { - return ; + return ( + + ); }; const ProgressBarAndroidToExport = React.forwardRef(ProgressBarAndroid); -/* $FlowFixMe(>=0.89.0 site=react_native_android_fb) This comment suppresses an - * error found when Flow v0.89 was deployed. To see the error, delete this - * comment and run Flow. */ -ProgressBarAndroidToExport.defaultProps = { - styleAttr: 'Normal', - indeterminate: true, - animating: true, -}; - /* $FlowFixMe(>=0.89.0 site=react_native_android_fb) This comment suppresses an * error found when Flow v0.89 was deployed. To see the error, delete this * comment and run Flow. */ diff --git a/Libraries/Components/ProgressBarAndroid/ProgressBarAndroidNativeComponent.js b/Libraries/Components/ProgressBarAndroid/ProgressBarAndroidNativeComponent.js index 0274e5866792e0..5b2fb1a222f99f 100644 --- a/Libraries/Components/ProgressBarAndroid/ProgressBarAndroidNativeComponent.js +++ b/Libraries/Components/ProgressBarAndroid/ProgressBarAndroidNativeComponent.js @@ -8,8 +8,6 @@ * @format */ -'use strict'; - import type {ColorValue} from '../../StyleSheet/StyleSheet'; import type {ViewProps} from '../View/ViewPropTypes'; import type {Double, WithDefault} from '../../Types/CodegenTypes'; diff --git a/Libraries/Components/ProgressBarAndroid/__tests__/__snapshots__/ProgressBarAndroid-test.js.snap b/Libraries/Components/ProgressBarAndroid/__tests__/__snapshots__/ProgressBarAndroid-test.js.snap index 87b8c16da4f277..f1985dfedb660a 100644 --- a/Libraries/Components/ProgressBarAndroid/__tests__/__snapshots__/ProgressBarAndroid-test.js.snap +++ b/Libraries/Components/ProgressBarAndroid/__tests__/__snapshots__/ProgressBarAndroid-test.js.snap @@ -18,7 +18,6 @@ exports[` should render as expected: should deep render wh exports[` should render as expected: should shallow render as when mocked 1`] = ` @@ -26,7 +25,6 @@ exports[` should render as expected: should shallow render exports[` should render as expected: should shallow render as when not mocked 1`] = ` diff --git a/Libraries/Components/ProgressViewIOS/ProgressViewIOS.android.js b/Libraries/Components/ProgressViewIOS/ProgressViewIOS.android.js index 8396ac4e87981e..d642a6545ab5bb 100644 --- a/Libraries/Components/ProgressViewIOS/ProgressViewIOS.android.js +++ b/Libraries/Components/ProgressViewIOS/ProgressViewIOS.android.js @@ -8,11 +8,10 @@ */ 'use strict'; - -const React = require('react'); -const StyleSheet = require('../../StyleSheet/StyleSheet'); -const Text = require('../../Text/Text'); -const View = require('../View/View'); +import * as React from 'react'; +import StyleSheet from '../../StyleSheet/StyleSheet'; +import Text from '../../Text/Text'; +import View from '../View/View'; class DummyProgressViewIOS extends React.Component { render() { diff --git a/Libraries/Components/ProgressViewIOS/ProgressViewIOS.ios.js b/Libraries/Components/ProgressViewIOS/ProgressViewIOS.ios.js index 189f7be3872ee0..d465440d174872 100644 --- a/Libraries/Components/ProgressViewIOS/ProgressViewIOS.ios.js +++ b/Libraries/Components/ProgressViewIOS/ProgressViewIOS.ios.js @@ -8,14 +8,11 @@ * @flow strict-local */ -'use strict'; - -const React = require('react'); -const StyleSheet = require('../../StyleSheet/StyleSheet'); +import * as React from 'react'; +import StyleSheet, {type ColorValue} from '../../StyleSheet/StyleSheet'; import RCTProgressViewNativeComponent from './RCTProgressViewNativeComponent'; import type {ImageSource} from '../../Image/ImageSource'; -import type {ColorValue} from '../../StyleSheet/StyleSheet'; import type {ViewProps} from '../View/ViewPropTypes'; type Props = $ReadOnly<{| diff --git a/Libraries/Components/ProgressViewIOS/RCTProgressViewNativeComponent.js b/Libraries/Components/ProgressViewIOS/RCTProgressViewNativeComponent.js index ccdd15e5100489..c0c0b56bfef280 100644 --- a/Libraries/Components/ProgressViewIOS/RCTProgressViewNativeComponent.js +++ b/Libraries/Components/ProgressViewIOS/RCTProgressViewNativeComponent.js @@ -8,8 +8,6 @@ * @format */ -'use strict'; - import type {Float, WithDefault} from '../../Types/CodegenTypes'; import type {ImageSource} from '../../Image/ImageSource'; import type {ColorValue} from '../../StyleSheet/StyleSheet'; diff --git a/Libraries/Components/RefreshControl/AndroidSwipeRefreshLayoutNativeComponent.js b/Libraries/Components/RefreshControl/AndroidSwipeRefreshLayoutNativeComponent.js index 9bd3600eb31571..de1b8d9a2a52f1 100644 --- a/Libraries/Components/RefreshControl/AndroidSwipeRefreshLayoutNativeComponent.js +++ b/Libraries/Components/RefreshControl/AndroidSwipeRefreshLayoutNativeComponent.js @@ -8,8 +8,6 @@ * @flow strict-local */ -'use strict'; - import * as React from 'react'; import codegenNativeCommands from 'react-native/Libraries/Utilities/codegenNativeCommands'; @@ -41,17 +39,9 @@ type NativeProps = $ReadOnly<{| */ progressBackgroundColor?: ?ColorValue, /** - * Size of the refresh indicator, see RefreshControl.SIZE. - * - * This type isn't currently accurate. It really is specific numbers - * hard coded in the Android platform. - * - * Also, 1 isn't actually a safe default. We are able to set this here - * because native code isn't currently consuming the generated artifact. - * This will end up being - * size?: WithDefault<'default' | 'large', 'default'>, + * Size of the refresh indicator. */ - size?: WithDefault, + size?: WithDefault<'default' | 'large', 'default'>, /** * Progress view top offset */ diff --git a/Libraries/Components/RefreshControl/PullToRefreshViewNativeComponent.js b/Libraries/Components/RefreshControl/PullToRefreshViewNativeComponent.js index 7f97db35b6120f..fa4791f2ab620c 100644 --- a/Libraries/Components/RefreshControl/PullToRefreshViewNativeComponent.js +++ b/Libraries/Components/RefreshControl/PullToRefreshViewNativeComponent.js @@ -8,9 +8,11 @@ * @flow strict-local */ -'use strict'; - -import type {DirectEventHandler, WithDefault} from '../../Types/CodegenTypes'; +import type { + DirectEventHandler, + Float, + WithDefault, +} from '../../Types/CodegenTypes'; import type {ColorValue} from '../../StyleSheet/StyleSheet'; import type {ViewProps} from '../View/ViewPropTypes'; import * as React from 'react'; @@ -34,6 +36,10 @@ type NativeProps = $ReadOnly<{| * The title displayed under the refresh indicator. */ title?: WithDefault, + /** + * Progress view top offset + */ + progressViewOffset?: WithDefault, /** * Called when the view starts refreshing. diff --git a/Libraries/Components/RefreshControl/RefreshControl.js b/Libraries/Components/RefreshControl/RefreshControl.js index a85c820791d1e1..0f0a16dbbccfc5 100644 --- a/Libraries/Components/RefreshControl/RefreshControl.js +++ b/Libraries/Components/RefreshControl/RefreshControl.js @@ -8,8 +8,6 @@ * @flow */ -'use strict'; - const Platform = require('../../Utilities/Platform'); const React = require('react'); @@ -22,18 +20,6 @@ import PullToRefreshViewNativeComponent, { Commands as PullToRefreshCommands, } from './PullToRefreshViewNativeComponent'; -let RefreshLayoutConsts: any; -if (Platform.OS === 'android') { - const AndroidSwipeRefreshLayout = require('../../ReactNative/UIManager').getViewManagerConfig( - 'AndroidSwipeRefreshLayout', - ); - RefreshLayoutConsts = AndroidSwipeRefreshLayout - ? AndroidSwipeRefreshLayout.Constants - : {SIZE: {}}; -} else { - RefreshLayoutConsts = {SIZE: {}}; -} - type IOSProps = $ReadOnly<{| /** * The color of the refresh indicator. @@ -63,16 +49,9 @@ type AndroidProps = $ReadOnly<{| */ progressBackgroundColor?: ?ColorValue, /** - * Size of the refresh indicator, see RefreshControl.SIZE. + * Size of the refresh indicator. */ - size?: ?( - | typeof RefreshLayoutConsts.SIZE.DEFAULT - | typeof RefreshLayoutConsts.SIZE.LARGE - ), - /** - * Progress view top offset - */ - progressViewOffset?: ?number, + size?: ?('default' | 'large'), |}>; export type RefreshControlProps = $ReadOnly<{| @@ -89,6 +68,11 @@ export type RefreshControlProps = $ReadOnly<{| * Whether the view should be indicating an active refresh. */ refreshing: boolean, + + /** + * Progress view top offset + */ + progressViewOffset?: ?number, |}>; /** @@ -137,8 +121,6 @@ export type RefreshControlProps = $ReadOnly<{| * in the `onRefresh` function otherwise the refresh indicator will stop immediately. */ class RefreshControl extends React.Component { - static SIZE: any = RefreshLayoutConsts.SIZE; - _nativeRef: ?React.ElementRef< | typeof PullToRefreshViewNativeComponent | typeof AndroidSwipeRefreshLayoutNativeComponent, @@ -181,7 +163,6 @@ class RefreshControl extends React.Component { colors, progressBackgroundColor, size, - progressViewOffset, ...props } = this.props; return ( diff --git a/Libraries/Components/RefreshControl/__mocks__/RefreshControlMock.js b/Libraries/Components/RefreshControl/__mocks__/RefreshControlMock.js index 40d41abbdc68ec..696eaa37ae7d2d 100644 --- a/Libraries/Components/RefreshControl/__mocks__/RefreshControlMock.js +++ b/Libraries/Components/RefreshControl/__mocks__/RefreshControlMock.js @@ -9,10 +9,9 @@ */ 'use strict'; +import * as React from 'react'; -const React = require('react'); - -const requireNativeComponent = require('../../../ReactNative/requireNativeComponent'); +import requireNativeComponent from '../../../ReactNative/requireNativeComponent'; import type {HostComponent} from '../../../Renderer/shims/ReactNativeTypes'; diff --git a/Libraries/Components/SafeAreaView/RCTSafeAreaViewNativeComponent.js b/Libraries/Components/SafeAreaView/RCTSafeAreaViewNativeComponent.js index 9025f1a88195d5..5365e35f8fce53 100644 --- a/Libraries/Components/SafeAreaView/RCTSafeAreaViewNativeComponent.js +++ b/Libraries/Components/SafeAreaView/RCTSafeAreaViewNativeComponent.js @@ -8,8 +8,6 @@ * @flow strict-local */ -'use strict'; - import type {ViewProps} from '../View/ViewPropTypes'; import type {WithDefault} from '../../Types/CodegenTypes'; diff --git a/Libraries/Components/SafeAreaView/SafeAreaView.js b/Libraries/Components/SafeAreaView/SafeAreaView.js index 8fc76bed849dc8..827965958439b5 100644 --- a/Libraries/Components/SafeAreaView/SafeAreaView.js +++ b/Libraries/Components/SafeAreaView/SafeAreaView.js @@ -59,4 +59,4 @@ if (Platform.OS !== 'ios') { ); } -module.exports = exported; +export default exported; diff --git a/Libraries/Components/SafeAreaView/__tests__/SafeAreaView-test.js b/Libraries/Components/SafeAreaView/__tests__/SafeAreaView-test.js index 133fd1da8a9f85..4b7edf60fee47e 100644 --- a/Libraries/Components/SafeAreaView/__tests__/SafeAreaView-test.js +++ b/Libraries/Components/SafeAreaView/__tests__/SafeAreaView-test.js @@ -12,7 +12,7 @@ 'use strict'; const React = require('react'); -const SafeAreaView = require('../SafeAreaView'); +import SafeAreaView from '../SafeAreaView'; const ReactNativeTestTools = require('../../../Utilities/ReactNativeTestTools'); const View = require('../../View/View'); const Text = require('../../../Text/Text'); diff --git a/Libraries/Components/ScrollResponder.js b/Libraries/Components/ScrollResponder.js deleted file mode 100644 index 7aef247b62c869..00000000000000 --- a/Libraries/Components/ScrollResponder.js +++ /dev/null @@ -1,772 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its 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 - */ - -'use strict'; - -const Dimensions = require('../Utilities/Dimensions'); -const FrameRateLogger = require('../Interaction/FrameRateLogger'); -const Keyboard = require('./Keyboard/Keyboard'); -const Platform = require('../Utilities/Platform'); -const React = require('react'); -const ReactNative = require('../Renderer/shims/ReactNative'); -const TextInputState = require('./TextInput/TextInputState'); -const UIManager = require('../ReactNative/UIManager'); - -const invariant = require('invariant'); - -import type {HostComponent} from '../Renderer/shims/ReactNativeTypes'; -import type {PressEvent, ScrollEvent} from '../Types/CoreEventTypes'; -import {type EventSubscription} from '../vendor/emitter/EventEmitter'; -import type {KeyboardEvent} from './Keyboard/Keyboard'; -import typeof ScrollView from './ScrollView/ScrollView'; -import type {Props as ScrollViewProps} from './ScrollView/ScrollView'; -import Commands from './ScrollView/ScrollViewCommands'; - -/** - * Mixin that can be integrated in order to handle scrolling that plays well - * with `ResponderEventPlugin`. Integrate with your platform specific scroll - * views, or even your custom built (every-frame animating) scroll views so that - * all of these systems play well with the `ResponderEventPlugin`. - * - * iOS scroll event timing nuances: - * =============================== - * - * - * Scrolling without bouncing, if you touch down: - * ------------------------------- - * - * 1. `onMomentumScrollBegin` (when animation begins after letting up) - * ... physical touch starts ... - * 2. `onTouchStartCapture` (when you press down to stop the scroll) - * 3. `onTouchStart` (same, but bubble phase) - * 4. `onResponderRelease` (when lifting up - you could pause forever before * lifting) - * 5. `onMomentumScrollEnd` - * - * - * Scrolling with bouncing, if you touch down: - * ------------------------------- - * - * 1. `onMomentumScrollBegin` (when animation begins after letting up) - * ... bounce begins ... - * ... some time elapses ... - * ... physical touch during bounce ... - * 2. `onMomentumScrollEnd` (Makes no sense why this occurs first during bounce) - * 3. `onTouchStartCapture` (immediately after `onMomentumScrollEnd`) - * 4. `onTouchStart` (same, but bubble phase) - * 5. `onTouchEnd` (You could hold the touch start for a long time) - * 6. `onMomentumScrollBegin` (When releasing the view starts bouncing back) - * - * So when we receive an `onTouchStart`, how can we tell if we are touching - * *during* an animation (which then causes the animation to stop)? The only way - * to tell is if the `touchStart` occurred immediately after the - * `onMomentumScrollEnd`. - * - * This is abstracted out for you, so you can just call this.scrollResponderIsAnimating() if - * necessary - * - * `ScrollResponder` also includes logic for blurring a currently focused input - * if one is focused while scrolling. The `ScrollResponder` is a natural place - * to put this logic since it can support not dismissing the keyboard while - * scrolling, unless a recognized "tap"-like gesture has occurred. - * - * The public lifecycle API includes events for keyboard interaction, responder - * interaction, and scrolling (among others). The keyboard callbacks - * `onKeyboardWill/Did/*` are *global* events, but are invoked on scroll - * responder's props so that you can guarantee that the scroll responder's - * internal state has been updated accordingly (and deterministically) by - * the time the props callbacks are invoke. Otherwise, you would always wonder - * if the scroll responder is currently in a state where it recognizes new - * keyboard positions etc. If coordinating scrolling with keyboard movement, - * *always* use these hooks instead of listening to your own global keyboard - * events. - * - * Public keyboard lifecycle API: (props callbacks) - * - * Standard Keyboard Appearance Sequence: - * - * this.props.onKeyboardWillShow - * this.props.onKeyboardDidShow - * - * `onScrollResponderKeyboardDismissed` will be invoked if an appropriate - * tap inside the scroll responder's scrollable region was responsible - * for the dismissal of the keyboard. There are other reasons why the - * keyboard could be dismissed. - * - * this.props.onScrollResponderKeyboardDismissed - * - * Standard Keyboard Hide Sequence: - * - * this.props.onKeyboardWillHide - * this.props.onKeyboardDidHide - */ - -const IS_ANIMATING_TOUCH_START_THRESHOLD_MS = 16; - -export type State = {| - isTouching: boolean, - lastMomentumScrollBeginTime: number, - lastMomentumScrollEndTime: number, - observedScrollSinceBecomingResponder: boolean, - becameResponderWhileAnimating: boolean, -|}; - -const ScrollResponderMixin = { - _subscriptionKeyboardWillShow: (null: ?EventSubscription), - _subscriptionKeyboardWillHide: (null: ?EventSubscription), - _subscriptionKeyboardDidShow: (null: ?EventSubscription), - _subscriptionKeyboardDidHide: (null: ?EventSubscription), - scrollResponderMixinGetInitialState: function(): State { - return { - isTouching: false, - lastMomentumScrollBeginTime: 0, - lastMomentumScrollEndTime: 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 !== 'always'). - observedScrollSinceBecomingResponder: false, - becameResponderWhileAnimating: false, - }; - }, - - /** - * Invoke this from an `onScroll` event. - */ - scrollResponderHandleScrollShouldSetResponder: function(): boolean { - // Allow any event touch pass through if the default pan responder is disabled - if (this.props.disableScrollViewPanResponder === true) { - return false; - } - return this.state.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: function( - e: PressEvent, - ): boolean { - // Allow any event touch pass through if the default pan responder is disabled - if (this.props.disableScrollViewPanResponder === true) { - return false; - } - - const currentlyFocusedInput = TextInputState.currentlyFocusedInput(); - - if ( - this.props.keyboardShouldPersistTaps === 'handled' && - currentlyFocusedInput != null && - e.target !== currentlyFocusedInput - ) { - return true; - } - 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 a text input, while the keyboard is - * up (which should dismiss the keyboard). - * - * Invoke this from an `onStartShouldSetResponderCapture` event. - */ - scrollResponderHandleStartShouldSetResponderCapture: function( - e: PressEvent, - ): boolean { - // The scroll view should receive taps instead of its descendants if: - // * it is already animating/decelerating - if (this.scrollResponderIsAnimating()) { - return true; - } - - // Allow any event touch pass through if the default pan responder is disabled - if (this.props.disableScrollViewPanResponder === true) { - return false; - } - - // * the keyboard is up, keyboardShouldPersistTaps is 'never' (the default), - // and a new touch starts with a non-textinput target (in which case the - // first tap should be sent to the scroll view and dismiss the keyboard, - // then the second tap goes to the actual interior view) - const currentlyFocusedTextInput = TextInputState.currentlyFocusedInput(); - const {keyboardShouldPersistTaps} = this.props; - const keyboardNeverPersistTaps = - !keyboardShouldPersistTaps || keyboardShouldPersistTaps === 'never'; - - if (typeof e.target === 'number') { - if (__DEV__) { - console.error( - 'Did not expect event target to be a number. Should have been a native component', - ); - } - - return false; - } - - if ( - keyboardNeverPersistTaps && - currentlyFocusedTextInput != null && - e.target != null && - !TextInputState.isTextInput(e.target) - ) { - return true; - } - - return false; - }, - - /** - * 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: function() {}, - - /** - * 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: function(): boolean { - return !this.state.observedScrollSinceBecomingResponder; - }, - - /** - * Invoke this from an `onTouchEnd` event. - * - * @param {PressEvent} e Event. - */ - scrollResponderHandleTouchEnd: function(e: PressEvent) { - const nativeEvent = e.nativeEvent; - this.state.isTouching = nativeEvent.touches.length !== 0; - this.props.onTouchEnd && this.props.onTouchEnd(e); - }, - - /** - * Invoke this from an `onTouchCancel` event. - * - * @param {PressEvent} e Event. - */ - scrollResponderHandleTouchCancel: function(e: PressEvent) { - this.state.isTouching = false; - this.props.onTouchCancel && this.props.onTouchCancel(e); - }, - - /** - * Invoke this from an `onResponderRelease` event. - */ - scrollResponderHandleResponderRelease: function(e: PressEvent) { - this.props.onResponderRelease && this.props.onResponderRelease(e); - - if (typeof e.target === 'number') { - if (__DEV__) { - console.error( - 'Did not expect event target to be a number. Should have been a native component', - ); - } - - return; - } - - // By default scroll views will unfocus a textField - // if another touch occurs outside of it - const currentlyFocusedTextInput = TextInputState.currentlyFocusedInput(); - if ( - this.props.keyboardShouldPersistTaps !== true && - this.props.keyboardShouldPersistTaps !== 'always' && - currentlyFocusedTextInput != null && - e.target !== currentlyFocusedTextInput && - !this.state.observedScrollSinceBecomingResponder && - !this.state.becameResponderWhileAnimating - ) { - this.props.onScrollResponderKeyboardDismissed && - this.props.onScrollResponderKeyboardDismissed(e); - TextInputState.blurTextInput(currentlyFocusedTextInput); - } - }, - - scrollResponderHandleScroll: function(e: ScrollEvent) { - (this: any).state.observedScrollSinceBecomingResponder = true; - (this: any).props.onScroll && (this: any).props.onScroll(e); - }, - - /** - * Invoke this from an `onResponderGrant` event. - */ - scrollResponderHandleResponderGrant: function(e: ScrollEvent) { - this.state.observedScrollSinceBecomingResponder = false; - this.props.onResponderGrant && this.props.onResponderGrant(e); - this.state.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: function(e: ScrollEvent) { - FrameRateLogger.beginScroll(); // TODO: track all scrolls after implementing onScrollEndAnimation - this.props.onScrollBeginDrag && this.props.onScrollBeginDrag(e); - }, - - /** - * Invoke this from an `onScrollEndDrag` event. - */ - scrollResponderHandleScrollEndDrag: function(e: ScrollEvent) { - const {velocity} = e.nativeEvent; - // - If we are animating, then this is a "drag" that is stopping the scrollview and momentum end - // will fire. - // - If velocity is non-zero, then the interaction will stop when momentum scroll ends or - // another drag starts and ends. - // - If we don't get velocity, better to stop the interaction twice than not stop it. - if ( - !this.scrollResponderIsAnimating() && - (!velocity || (velocity.x === 0 && velocity.y === 0)) - ) { - FrameRateLogger.endScroll(); - } - this.props.onScrollEndDrag && this.props.onScrollEndDrag(e); - }, - - /** - * Invoke this from an `onMomentumScrollBegin` event. - */ - scrollResponderHandleMomentumScrollBegin: function(e: ScrollEvent) { - this.state.lastMomentumScrollBeginTime = global.performance.now(); - this.props.onMomentumScrollBegin && this.props.onMomentumScrollBegin(e); - }, - - /** - * Invoke this from an `onMomentumScrollEnd` event. - */ - scrollResponderHandleMomentumScrollEnd: function(e: ScrollEvent) { - FrameRateLogger.endScroll(); - this.state.lastMomentumScrollEndTime = global.performance.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 {PressEvent} e Touch Start event. - */ - scrollResponderHandleTouchStart: function(e: PressEvent) { - this.state.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 {PressEvent} e Touch Start event. - */ - scrollResponderHandleTouchMove: function(e: PressEvent) { - 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: function(): boolean { - const now = global.performance.now(); - const timeSinceLastMomentumScrollEnd = - now - this.state.lastMomentumScrollEndTime; - const isAnimating = - timeSinceLastMomentumScrollEnd < IS_ANIMATING_TOUCH_START_THRESHOLD_MS || - this.state.lastMomentumScrollEndTime < - this.state.lastMomentumScrollBeginTime; - return isAnimating; - }, - - /** - * Returns the node that represents native view that can be scrolled. - * Components can pass what node to use by defining a `getScrollableNode` - * function otherwise `this` is used. - */ - scrollResponderGetScrollableNode: function(): ?number { - return this.getScrollableNode - ? this.getScrollableNode() - : ReactNative.findNodeHandle(this); - }, - - /** - * A helper function to scroll to a specific point in the ScrollView. - * This is currently used to help focus 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: function( - 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 || {}); - } - - const that: React.ElementRef = (this: any); - invariant( - that.getNativeScrollRef != null, - 'Expected scrollTo to be called on a scrollViewRef. If this exception occurs it is likely a bug in React Native', - ); - const nativeScrollRef = that.getNativeScrollRef(); - if (nativeScrollRef == null) { - return; - } - Commands.scrollTo(nativeScrollRef, x || 0, y || 0, animated !== false); - }, - - /** - * Scrolls to the end of the ScrollView, either immediately or with a smooth - * animation. - * - * Example: - * - * `scrollResponderScrollToEnd({animated: true})` - */ - scrollResponderScrollToEnd: function(options?: {animated?: boolean, ...}) { - // Default to true - const animated = (options && options.animated) !== false; - - const that: React.ElementRef = (this: any); - invariant( - that.getNativeScrollRef != null, - 'Expected scrollToEnd to be called on a scrollViewRef. If this exception occurs it is likely a bug in React Native', - ); - const nativeScrollRef = that.getNativeScrollRef(); - if (nativeScrollRef == null) { - return; - } - - Commands.scrollToEnd(nativeScrollRef, animated); - }, - - /** - * 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: function( - rect: {| - x: number, - y: number, - width: number, - height: number, - animated?: boolean, - |}, - animated?: boolean, // deprecated, put this inside the rect argument instead - ) { - invariant(Platform.OS === 'ios', 'zoomToRect is not implemented'); - if ('animated' in rect) { - animated = rect.animated; - delete rect.animated; - } else if (typeof animated !== 'undefined') { - console.warn( - '`scrollResponderZoomTo` `animated` argument is deprecated. Use `options.animated` instead', - ); - } - - const that: React.ElementRef = this; - invariant( - that.getNativeScrollRef != null, - 'Expected zoomToRect to be called on a scrollViewRef. If this exception occurs it is likely a bug in React Native', - ); - const nativeScrollRef = that.getNativeScrollRef(); - if (nativeScrollRef == null) { - return; - } - Commands.zoomToRect(nativeScrollRef, rect, animated !== false); - }, - - /** - * Displays the scroll indicators momentarily. - */ - scrollResponderFlashScrollIndicators: function() { - const that: React.ElementRef = (this: any); - invariant( - that.getNativeScrollRef != null, - 'Expected flashScrollIndicators to be called on a scrollViewRef. If this exception occurs it is likely a bug in React Native', - ); - const nativeScrollRef = that.getNativeScrollRef(); - if (nativeScrollRef == null) { - return; - } - Commands.flashScrollIndicators(nativeScrollRef); - }, - - /** - * 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 {number} nodeHandle The TextInput node handle - * @param {number} additionalOffset The scroll view's bottom "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: function( - nodeHandle: number | React.ElementRef>, - additionalOffset?: number, - preventNegativeScrollOffset?: boolean, - ) { - this.additionalScrollOffset = additionalOffset || 0; - this.preventNegativeScrollOffset = !!preventNegativeScrollOffset; - - if (typeof nodeHandle === 'number') { - UIManager.measureLayout( - nodeHandle, - ReactNative.findNodeHandle(this.getInnerViewNode()), - this.scrollResponderTextInputFocusError, - this.scrollResponderInputMeasureAndScrollToKeyboard, - ); - } else { - const innerRef = this.getInnerViewRef(); - - if (innerRef == null) { - return; - } - - nodeHandle.measureLayout( - innerRef, - this.scrollResponderInputMeasureAndScrollToKeyboard, - this.scrollResponderTextInputFocusError, - ); - } - }, - - /** - * 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: function( - 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: function(msg: string) { - console.error('Error measuring text field: ', msg); - }, - - /** - * `componentWillMount` is the closest thing to a standard "constructor" for - * React components. - * - * The `keyboardWillShow` is called before input focus. - */ - UNSAFE_componentWillMount: function() { - const {keyboardShouldPersistTaps} = ((this: any).props: ScrollViewProps); - if (typeof keyboardShouldPersistTaps === 'boolean') { - console.warn( - `'keyboardShouldPersistTaps={${ - keyboardShouldPersistTaps === true ? 'true' : 'false' - }}' is deprecated. ` + - `Use 'keyboardShouldPersistTaps="${ - keyboardShouldPersistTaps ? 'always' : 'never' - }"' instead`, - ); - } - - (this: any).keyboardWillOpenTo = null; - (this: any).additionalScrollOffset = 0; - this._subscriptionKeyboardWillShow = Keyboard.addListener( - 'keyboardWillShow', - this.scrollResponderKeyboardWillShow, - ); - - this._subscriptionKeyboardWillHide = Keyboard.addListener( - 'keyboardWillHide', - this.scrollResponderKeyboardWillHide, - ); - this._subscriptionKeyboardDidShow = Keyboard.addListener( - 'keyboardDidShow', - this.scrollResponderKeyboardDidShow, - ); - this._subscriptionKeyboardDidHide = Keyboard.addListener( - 'keyboardDidHide', - this.scrollResponderKeyboardDidHide, - ); - }, - - componentWillUnmount: function() { - if (this._subscriptionKeyboardWillShow != null) { - this._subscriptionKeyboardWillShow.remove(); - } - if (this._subscriptionKeyboardWillHide != null) { - this._subscriptionKeyboardWillHide.remove(); - } - if (this._subscriptionKeyboardDidShow != null) { - this._subscriptionKeyboardDidShow.remove(); - } - if (this._subscriptionKeyboardDidHide != null) { - this._subscriptionKeyboardDidHide.remove(); - } - }, - - /** - * 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` module 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: function(e: KeyboardEvent) { - this.keyboardWillOpenTo = e; - this.props.onKeyboardWillShow && this.props.onKeyboardWillShow(e); - }, - - scrollResponderKeyboardWillHide: function(e: KeyboardEvent) { - this.keyboardWillOpenTo = null; - this.props.onKeyboardWillHide && this.props.onKeyboardWillHide(e); - }, - - scrollResponderKeyboardDidShow: function(e: KeyboardEvent) { - // 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: function(e: KeyboardEvent) { - this.keyboardWillOpenTo = null; - this.props.onKeyboardDidHide && this.props.onKeyboardDidHide(e); - }, -}; - -const ScrollResponder = { - Mixin: ScrollResponderMixin, -}; - -module.exports = ScrollResponder; diff --git a/Libraries/Components/ScrollView/AndroidHorizontalScrollContentViewNativeComponent.js b/Libraries/Components/ScrollView/AndroidHorizontalScrollContentViewNativeComponent.js index fa0a173a4fdcc5..71210f989d7e20 100644 --- a/Libraries/Components/ScrollView/AndroidHorizontalScrollContentViewNativeComponent.js +++ b/Libraries/Components/ScrollView/AndroidHorizontalScrollContentViewNativeComponent.js @@ -5,36 +5,19 @@ * LICENSE file in the root directory of this source tree. * * @format - * @flow + * @flow strict-local */ -'use strict'; - -import registerGeneratedViewConfig from '../../Utilities/registerGeneratedViewConfig'; -import requireNativeComponent from '../../ReactNative/requireNativeComponent'; - +import codegenNativeComponent from '../../Utilities/codegenNativeComponent'; import type {HostComponent} from '../../Renderer/shims/ReactNativeTypes'; import type {ViewProps} from '../View/ViewPropTypes'; -const AndroidHorizontalScrollContentViewViewConfig = { - uiViewClassName: 'AndroidHorizontalScrollContentView', - bubblingEventTypes: {}, - directEventTypes: {}, - validAttributes: {}, -}; +type NativeProps = $ReadOnly<{| + ...ViewProps, +|}>; -let AndroidHorizontalScrollContentViewNativeComponent; -if (global.RN$Bridgeless) { - registerGeneratedViewConfig( - 'AndroidHorizontalScrollContentView', - AndroidHorizontalScrollContentViewViewConfig, - ); - AndroidHorizontalScrollContentViewNativeComponent = - 'AndroidHorizontalScrollContentView'; -} else { - AndroidHorizontalScrollContentViewNativeComponent = requireNativeComponent( - 'AndroidHorizontalScrollContentView', - ); -} +type NativeType = HostComponent; -export default ((AndroidHorizontalScrollContentViewNativeComponent: any): HostComponent); +export default (codegenNativeComponent( + 'AndroidHorizontalScrollContentView', +): NativeType); diff --git a/Libraries/Components/ScrollView/AndroidHorizontalScrollViewNativeComponent.js b/Libraries/Components/ScrollView/AndroidHorizontalScrollViewNativeComponent.js index a4a0d4e6bb8dba..05f629e01a1d1e 100644 --- a/Libraries/Components/ScrollView/AndroidHorizontalScrollViewNativeComponent.js +++ b/Libraries/Components/ScrollView/AndroidHorizontalScrollViewNativeComponent.js @@ -4,54 +4,40 @@ * 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 - * @flow */ -'use strict'; - -import registerGeneratedViewConfig from '../../Utilities/registerGeneratedViewConfig'; -import requireNativeComponent from '../../ReactNative/requireNativeComponent'; - +import type {ScrollViewNativeProps as Props} from './ScrollViewNativeComponentType'; import type {HostComponent} from '../../Renderer/shims/ReactNativeTypes'; -import type {ScrollViewNativeProps} from './ScrollViewNativeComponentType'; - -const AndroidHorizontalScrollViewViewConfig = { - uiViewClassName: 'AndroidHorizontalScrollView', - bubblingEventTypes: {}, - directEventTypes: {}, - validAttributes: { - decelerationRate: true, - disableIntervalMomentum: true, - endFillColor: {process: require('../../StyleSheet/processColor')}, - fadingEdgeLength: true, - nestedScrollEnabled: true, - overScrollMode: true, - pagingEnabled: true, - persistentScrollbar: true, - scrollEnabled: true, - scrollPerfTag: true, - sendMomentumEvents: true, - showsHorizontalScrollIndicator: true, - snapToEnd: true, - snapToInterval: true, - snapToStart: true, - snapToOffsets: true, - contentOffset: true, - }, -}; +import * as NativeComponentRegistry from '../../NativeComponent/NativeComponentRegistry'; -let AndroidHorizontalScrollViewNativeComponent; -if (global.RN$Bridgeless) { - registerGeneratedViewConfig( - 'AndroidHorizontalScrollView', - AndroidHorizontalScrollViewViewConfig, - ); - AndroidHorizontalScrollViewNativeComponent = 'AndroidHorizontalScrollView'; -} else { - AndroidHorizontalScrollViewNativeComponent = requireNativeComponent( - 'AndroidHorizontalScrollView', - ); -} +const AndroidHorizontalScrollViewNativeComponent: HostComponent = NativeComponentRegistry.get( + 'AndroidHorizontalScrollView', + () => ({ + uiViewClassName: 'AndroidHorizontalScrollView', + bubblingEventTypes: {}, + directEventTypes: {}, + validAttributes: { + decelerationRate: true, + disableIntervalMomentum: true, + endFillColor: {process: require('../../StyleSheet/processColor')}, + fadingEdgeLength: true, + nestedScrollEnabled: true, + overScrollMode: true, + pagingEnabled: true, + persistentScrollbar: true, + scrollEnabled: true, + scrollPerfTag: true, + sendMomentumEvents: true, + showsHorizontalScrollIndicator: true, + snapToEnd: true, + snapToInterval: true, + snapToStart: true, + snapToOffsets: true, + contentOffset: true, + }, + }), +); -export default ((AndroidHorizontalScrollViewNativeComponent: any): HostComponent); +export default AndroidHorizontalScrollViewNativeComponent; diff --git a/Libraries/Components/ScrollView/ScrollContentViewNativeComponent.js b/Libraries/Components/ScrollView/ScrollContentViewNativeComponent.js index 6a94a30a72dae7..4e3174044dfc8b 100644 --- a/Libraries/Components/ScrollView/ScrollContentViewNativeComponent.js +++ b/Libraries/Components/ScrollView/ScrollContentViewNativeComponent.js @@ -8,32 +8,18 @@ * @flow */ -'use strict'; - -import registerGeneratedViewConfig from '../../Utilities/registerGeneratedViewConfig'; -import requireNativeComponent from '../../ReactNative/requireNativeComponent'; - import type {HostComponent} from '../../Renderer/shims/ReactNativeTypes'; -import type {ViewProps} from '../View/ViewPropTypes'; - -const ScrollContentViewViewConfig = { - uiViewClassName: 'RCTScrollContentView', - bubblingEventTypes: {}, - directEventTypes: {}, - validAttributes: {}, -}; +import * as NativeComponentRegistry from '../../NativeComponent/NativeComponentRegistry'; +import type {ViewProps as Props} from '../View/ViewPropTypes'; -let ScrollContentViewNativeComponent; -if (global.RN$Bridgeless) { - registerGeneratedViewConfig( - 'RCTScrollContentView', - ScrollContentViewViewConfig, - ); - ScrollContentViewNativeComponent = 'RCTScrollContentView'; -} else { - ScrollContentViewNativeComponent = requireNativeComponent( - 'RCTScrollContentView', - ); -} +const ScrollContentViewNativeComponent: HostComponent = NativeComponentRegistry.get( + 'RCTScrollContentView', + () => ({ + uiViewClassName: 'RCTScrollContentView', + bubblingEventTypes: {}, + directEventTypes: {}, + validAttributes: {}, + }), +); -export default ((ScrollContentViewNativeComponent: any): HostComponent); +export default ScrollContentViewNativeComponent; diff --git a/Libraries/Components/ScrollView/ScrollView.js b/Libraries/Components/ScrollView/ScrollView.js index cdaafbe4a6b2af..541245effac1c2 100644 --- a/Libraries/Components/ScrollView/ScrollView.js +++ b/Libraries/Components/ScrollView/ScrollView.js @@ -8,23 +8,24 @@ * @flow strict-local */ -'use strict'; - import AnimatedImplementation from '../../Animated/AnimatedImplementation'; +import Dimensions from '../../Utilities/Dimensions'; import Platform from '../../Utilities/Platform'; import * as React from 'react'; import ReactNative from '../../Renderer/shims/ReactNative'; require('../../Renderer/shims/ReactNative'); // Force side effects to prevent T55744311 -import ScrollResponder from '../ScrollResponder'; import ScrollViewStickyHeader from './ScrollViewStickyHeader'; import StyleSheet from '../../StyleSheet/StyleSheet'; import View from '../View/View'; +import UIManager from '../../ReactNative/UIManager'; +import Keyboard from '../Keyboard/Keyboard'; +import FrameRateLogger from '../../Interaction/FrameRateLogger'; +import TextInputState from '../TextInput/TextInputState'; import dismissKeyboard from '../../Utilities/dismissKeyboard'; import flattenStyle from '../../StyleSheet/flattenStyle'; import invariant from 'invariant'; import processDecelerationRate from './processDecelerationRate'; -import resolveAssetSource from '../../Image/resolveAssetSource'; import splitLayoutProps from '../../StyleSheet/splitLayoutProps'; import setAndForwardRef from '../../Utilities/setAndForwardRef'; @@ -38,33 +39,110 @@ import type { LayoutEvent, } from '../../Types/CoreEventTypes'; import type {HostComponent} from '../../Renderer/shims/ReactNativeTypes'; -import type {State as ScrollResponderState} from '../ScrollResponder'; import type {ViewProps} from '../View/ViewPropTypes'; +import ScrollViewContext, {HORIZONTAL, VERTICAL} from './ScrollViewContext'; import type {Props as ScrollViewStickyHeaderProps} from './ScrollViewStickyHeader'; +import type {KeyboardEvent} from '../Keyboard/Keyboard'; +import type {EventSubscription} from '../../vendor/emitter/EventEmitter'; -import ScrollViewContext, {HORIZONTAL, VERTICAL} from './ScrollViewContext'; -import ScrollViewNativeComponent from './ScrollViewNativeComponent'; -import ScrollContentViewNativeComponent from './ScrollContentViewNativeComponent'; -import AndroidHorizontalScrollViewNativeComponent from './AndroidHorizontalScrollViewNativeComponent'; +import Commands from './ScrollViewCommands'; import AndroidHorizontalScrollContentViewNativeComponent from './AndroidHorizontalScrollContentViewNativeComponent'; +import AndroidHorizontalScrollViewNativeComponent from './AndroidHorizontalScrollViewNativeComponent'; +import ScrollContentViewNativeComponent from './ScrollContentViewNativeComponent'; +import ScrollViewNativeComponent from './ScrollViewNativeComponent'; -let AndroidScrollView; -let AndroidHorizontalScrollContentView; -let AndroidHorizontalScrollView; -let RCTScrollView; -let RCTScrollContentView; - -if (Platform.OS === 'android') { - AndroidScrollView = ScrollViewNativeComponent; - AndroidHorizontalScrollView = AndroidHorizontalScrollViewNativeComponent; - AndroidHorizontalScrollContentView = AndroidHorizontalScrollContentViewNativeComponent; -} else if ( - Platform.OS === 'ios' || - Platform.OS === 'macos' /*TODO(macOS GH#774)*/ -) { - RCTScrollView = ScrollViewNativeComponent; - RCTScrollContentView = ScrollContentViewNativeComponent; -} +const {NativeHorizontalScrollViewTuple, NativeVerticalScrollViewTuple} = + Platform.OS === 'android' + ? { + NativeHorizontalScrollViewTuple: [ + AndroidHorizontalScrollViewNativeComponent, + AndroidHorizontalScrollContentViewNativeComponent, + ], + NativeVerticalScrollViewTuple: [ScrollViewNativeComponent, View], + } + : { + NativeHorizontalScrollViewTuple: [ + ScrollViewNativeComponent, + ScrollContentViewNativeComponent, + ], + NativeVerticalScrollViewTuple: [ + ScrollViewNativeComponent, + ScrollContentViewNativeComponent, + ], + }; + +/* + * iOS scroll event timing nuances: + * =============================== + * + * + * Scrolling without bouncing, if you touch down: + * ------------------------------- + * + * 1. `onMomentumScrollBegin` (when animation begins after letting up) + * ... physical touch starts ... + * 2. `onTouchStartCapture` (when you press down to stop the scroll) + * 3. `onTouchStart` (same, but bubble phase) + * 4. `onResponderRelease` (when lifting up - you could pause forever before * lifting) + * 5. `onMomentumScrollEnd` + * + * + * Scrolling with bouncing, if you touch down: + * ------------------------------- + * + * 1. `onMomentumScrollBegin` (when animation begins after letting up) + * ... bounce begins ... + * ... some time elapses ... + * ... physical touch during bounce ... + * 2. `onMomentumScrollEnd` (Makes no sense why this occurs first during bounce) + * 3. `onTouchStartCapture` (immediately after `onMomentumScrollEnd`) + * 4. `onTouchStart` (same, but bubble phase) + * 5. `onTouchEnd` (You could hold the touch start for a long time) + * 6. `onMomentumScrollBegin` (When releasing the view starts bouncing back) + * + * So when we receive an `onTouchStart`, how can we tell if we are touching + * *during* an animation (which then causes the animation to stop)? The only way + * to tell is if the `touchStart` occurred immediately after the + * `onMomentumScrollEnd`. + * + * This is abstracted out for you, so you can just call this.scrollResponderIsAnimating() if + * necessary + * + * `ScrollView` also includes logic for blurring a currently focused input + * if one is focused while scrolling. This is a natural place + * to put this logic since it can support not dismissing the keyboard while + * scrolling, unless a recognized "tap"-like gesture has occurred. + * + * The public lifecycle API includes events for keyboard interaction, responder + * interaction, and scrolling (among others). The keyboard callbacks + * `onKeyboardWill/Did/*` are *global* events, but are invoked on scroll + * responder's props so that you can guarantee that the scroll responder's + * internal state has been updated accordingly (and deterministically) by + * the time the props callbacks are invoke. Otherwise, you would always wonder + * if the scroll responder is currently in a state where it recognizes new + * keyboard positions etc. If coordinating scrolling with keyboard movement, + * *always* use these hooks instead of listening to your own global keyboard + * events. + * + * Public keyboard lifecycle API: (props callbacks) + * + * Standard Keyboard Appearance Sequence: + * + * this.props.onKeyboardWillShow + * this.props.onKeyboardDidShow + * + * `onScrollResponderKeyboardDismissed` will be invoked if an appropriate + * tap inside the scroll responder's scrollable region was responsible + * for the dismissal of the keyboard. There are other reasons why the + * keyboard could be dismissed. + * + * this.props.onScrollResponderKeyboardDismissed + * + * Standard Keyboard Hide Sequence: + * + * this.props.onKeyboardWillHide + * this.props.onKeyboardDidHide + */ // Public methods for ScrollView export type ScrollViewImperativeMethods = $ReadOnly<{| @@ -76,14 +154,9 @@ export type ScrollViewImperativeMethods = $ReadOnly<{| scrollTo: $PropertyType, scrollToEnd: $PropertyType, flashScrollIndicators: $PropertyType, - - // ScrollResponder.Mixin public methods - scrollResponderZoomTo: $PropertyType< - typeof ScrollResponder.Mixin, - 'scrollResponderZoomTo', - >, + scrollResponderZoomTo: $PropertyType, scrollResponderScrollNativeHandleToKeyboard: $PropertyType< - typeof ScrollResponder.Mixin, + ScrollView, 'scrollResponderScrollNativeHandleToKeyboard', >, |}>; @@ -98,6 +171,12 @@ type IOSProps = $ReadOnly<{| * @platform ios */ automaticallyAdjustContentInsets?: ?boolean, + /** + * Controls whether iOS should automatically adjust the scroll indicator + * insets. The default value is true. Available on iOS 13 and later. + * @platform ios + */ + automaticallyAdjustsScrollIndicatorInsets?: ?boolean, /** * The amount by which the scroll view content is inset from the edges * of the scroll view. Defaults to `{top: 0, left: 0, bottom: 0, right: 0}`. @@ -368,32 +447,15 @@ type AndroidProps = $ReadOnly<{| fadingEdgeLength?: ?number, |}>; -type VRProps = $ReadOnly<{| - /** - * Optionally an image can be used for the scroll bar thumb. This will - * override the color. While the image is loading or the image fails to - * load the color will be used instead. Use an alpha of 0 in the color - * to avoid seeing it while the image is loading. - * - * - `uri` - a string representing the resource identifier for the image, which - * should be either a local file path or the name of a static image resource - * - `number` - Opaque type returned by something like - * `import IMAGE from './image.jpg'`. - * @platform vr - */ - scrollBarThumbImage?: ?($ReadOnly<{||}> | number), // Opaque type returned by import IMAGE from './image.jpg' -|}>; - type StickyHeaderComponentType = React.AbstractComponent< ScrollViewStickyHeaderProps, - $ReadOnly<{setNextHeaderY: number => void, ...}>, + $ReadOnly void}>, >; export type Props = $ReadOnly<{| ...ViewProps, ...IOSProps, ...AndroidProps, - ...VRProps, /** * These styles will be applied to the scroll view content container which @@ -456,10 +518,8 @@ export type Props = $ReadOnly<{| * On android this is not supported and it will have the same behavior as 'none'. */ keyboardDismissMode?: ?// default - (| 'none' // cross-platform - | 'on-drag' - | 'interactive' - ), // ios only + // cross-platform + ('none' | 'on-drag' | 'interactive'), // ios only /** * Determines when the keyboard should stay visible after a tap. * @@ -506,7 +566,10 @@ export type Props = $ReadOnly<{| * which this ScrollView renders. */ onContentSizeChange?: (contentWidth: number, contentHeight: number) => void, - onKeyboardDidShow?: (event: PressEvent) => void, + onKeyboardDidShow?: (event: KeyboardEvent) => void, + onKeyboardDidHide?: (event: KeyboardEvent) => void, + onKeyboardWillShow?: (event: KeyboardEvent) => void, + onKeyboardWillHide?: (event: KeyboardEvent) => void, /** * When true, the scroll view stops on multiples of the scroll view's size * when scrolling. This can be used for horizontal pagination. The default @@ -528,6 +591,11 @@ export type Props = $ReadOnly<{| * The default value is true. */ showsVerticalScrollIndicator?: ?boolean, + /** + * When true, Sticky header is hidden when scrolling down, and dock at the top + * when scrolling up + */ + stickyHeaderHiddenOnScroll?: ?boolean, /** * An array of child indices determining which children get docked to the * top of the screen when scrolling. For example, passing @@ -591,7 +659,8 @@ export type Props = $ReadOnly<{| * * See [RefreshControl](docs/refreshcontrol.html). */ - // $FlowFixMe - how to handle generic type without existential operator? + /* $FlowFixMe[unclear-type] - how to handle generic type without existential + * operator? */ refreshControl?: ?React.Element, children?: React.Node, /** @@ -612,22 +681,9 @@ export type Props = $ReadOnly<{| type State = {| contentKey: number, // TODO(macOS GH#774) layoutHeight: ?number, - ...ScrollResponderState, |}; -function createScrollResponder( - node: React.ElementRef, -): typeof ScrollResponder.Mixin { - const scrollResponder = {...ScrollResponder.Mixin}; - - for (const key in scrollResponder) { - if (typeof scrollResponder[key] === 'function') { - scrollResponder[key] = scrollResponder[key].bind(node); - } - } - - return scrollResponder; -} +const IS_ANIMATING_TOUCH_START_THRESHOLD_MS = 16; type ScrollViewComponentStatics = $ReadOnly<{| Context: typeof ScrollViewContext, @@ -670,61 +726,17 @@ type ScrollViewComponentStatics = $ReadOnly<{| */ class ScrollView extends React.Component { static Context: typeof ScrollViewContext = ScrollViewContext; - /** - * Part 1: Removing ScrollResponder.Mixin: - * - * 1. Mixin methods should be flow typed. That's why we create a - * copy of ScrollResponder.Mixin and attach it to this._scrollResponder. - * Otherwise, we'd have to manually declare each method on the component - * class and assign it a flow type. - * 2. Mixin methods can call component methods, and access the component's - * props and state. So, we need to bind all mixin methods to the - * component instance. - * 3. Continued... - */ - _scrollResponder: typeof ScrollResponder.Mixin = createScrollResponder(this); constructor(props: Props) { super(props); - /** - * Part 2: Removing ScrollResponder.Mixin - * - * 3. Mixin methods access other mixin methods via dynamic dispatch using - * this. Since mixin methods are bound to the component instance, we need - * to copy all mixin methods to the component instance. This is also - * necessary because getScrollResponder() is a public method that returns - * an object that can be used to execute all scrollResponder methods. - * Since the object returned from that method is the ScrollView instance, - * we need to bind all mixin methods to the ScrollView instance. - */ - for (const key in ScrollResponder.Mixin) { - if ( - typeof ScrollResponder.Mixin[key] === 'function' && - key.startsWith('scrollResponder') - ) { - // $FlowFixMe - dynamically adding properties to a class - (this: any)[key] = ScrollResponder.Mixin[key].bind(this); - } - } - - /** - * Part 3: Removing ScrollResponder.Mixin - * - * 4. Mixins can initialize properties and use properties on the component - * instance. - */ - Object.keys(ScrollResponder.Mixin) - .filter(key => typeof ScrollResponder.Mixin[key] !== 'function') - .forEach(key => { - // $FlowFixMe - dynamically adding properties to a class - (this: any)[key] = ScrollResponder.Mixin[key]; - }); + this._scrollAnimatedValue = new AnimatedImplementation.Value( + this.props.contentOffset?.y ?? 0, + ); + this._scrollAnimatedValue.setOffset(this.props.contentInset?.top ?? 0); } - _scrollAnimatedValue: AnimatedImplementation.Value = new AnimatedImplementation.Value( - 0, - ); + _scrollAnimatedValue: AnimatedImplementation.Value; _scrollAnimatedValueAttachment: ?{detach: () => void, ...} = null; _stickyHeaderRefs: Map< string, @@ -732,44 +744,96 @@ class ScrollView extends React.Component { > = new Map(); _headerLayoutYs: Map = new Map(); + _keyboardWillOpenTo: ?KeyboardEvent = null; + _additionalScrollOffset: number = 0; + _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 !== 'always'). + _observedScrollSinceBecomingResponder: boolean = false; + _becameResponderWhileAnimating: boolean = false; + _preventNegativeScrollOffset: ?boolean = null; + + _animated = null; + + _subscriptionKeyboardWillShow: ?EventSubscription = null; + _subscriptionKeyboardWillHide: ?EventSubscription = null; + _subscriptionKeyboardDidShow: ?EventSubscription = null; + _subscriptionKeyboardDidHide: ?EventSubscription = null; + state: State = { contentKey: 1, // TODO(macOS GH#774) layoutHeight: null, - ...ScrollResponder.Mixin.scrollResponderMixinGetInitialState(), }; - UNSAFE_componentWillMount() { - this._scrollResponder.UNSAFE_componentWillMount(); - this._scrollAnimatedValue = new AnimatedImplementation.Value( - this.props.contentOffset?.y ?? 0, + componentDidMount() { + if (typeof this.props.keyboardShouldPersistTaps === 'boolean') { + console.warn( + `'keyboardShouldPersistTaps={${ + this.props.keyboardShouldPersistTaps === true ? 'true' : 'false' + }}' is deprecated. ` + + `Use 'keyboardShouldPersistTaps="${ + this.props.keyboardShouldPersistTaps ? 'always' : 'never' + }"' instead`, + ); + } + + this._keyboardWillOpenTo = null; + this._additionalScrollOffset = 0; + + this._subscriptionKeyboardWillShow = Keyboard.addListener( + 'keyboardWillShow', + this.scrollResponderKeyboardWillShow, ); - this._scrollAnimatedValue.setOffset(this.props.contentInset?.top ?? 0); - this._stickyHeaderRefs = new Map(); - this._headerLayoutYs = new Map(); + this._subscriptionKeyboardWillHide = Keyboard.addListener( + 'keyboardWillHide', + this.scrollResponderKeyboardWillHide, + ); + this._subscriptionKeyboardDidShow = Keyboard.addListener( + 'keyboardDidShow', + this.scrollResponderKeyboardDidShow, + ); + this._subscriptionKeyboardDidHide = Keyboard.addListener( + 'keyboardDidHide', + this.scrollResponderKeyboardDidHide, + ); + + this._updateAnimatedNodeAttachment(); } - UNSAFE_componentWillReceiveProps(nextProps: Props) { - const currentContentInsetTop = this.props.contentInset - ? this.props.contentInset.top + componentDidUpdate(prevProps: Props) { + const prevContentInsetTop = prevProps.contentInset + ? prevProps.contentInset.top : 0; - const nextContentInsetTop = nextProps.contentInset - ? nextProps.contentInset.top + const newContentInsetTop = this.props.contentInset + ? this.props.contentInset.top : 0; - if (currentContentInsetTop !== nextContentInsetTop) { - this._scrollAnimatedValue.setOffset(nextContentInsetTop || 0); + if (prevContentInsetTop !== newContentInsetTop) { + this._scrollAnimatedValue.setOffset(newContentInsetTop || 0); } - } - componentDidMount() { - this._updateAnimatedNodeAttachment(); - } - - componentDidUpdate() { this._updateAnimatedNodeAttachment(); } componentWillUnmount() { - this._scrollResponder.componentWillUnmount(); + if (this._subscriptionKeyboardWillShow != null) { + this._subscriptionKeyboardWillShow.remove(); + } + if (this._subscriptionKeyboardWillHide != null) { + this._subscriptionKeyboardWillHide.remove(); + } + if (this._subscriptionKeyboardDidShow != null) { + this._subscriptionKeyboardDidShow.remove(); + } + if (this._subscriptionKeyboardDidHide != null) { + this._subscriptionKeyboardDidHide.remove(); + } + if (this._scrollAnimatedValueAttachment) { this._scrollAnimatedValueAttachment.detach(); } @@ -798,10 +862,7 @@ class ScrollView extends React.Component { ref.scrollTo = this.scrollTo; ref.scrollToEnd = this.scrollToEnd; ref.flashScrollIndicators = this.flashScrollIndicators; - - // $FlowFixMe - This method was manually bound from ScrollResponder.mixin ref.scrollResponderZoomTo = this.scrollResponderZoomTo; - // $FlowFixMe - This method was manually bound from ScrollResponder.mixin ref.scrollResponderScrollNativeHandleToKeyboard = this.scrollResponderScrollNativeHandleToKeyboard; } }, @@ -814,7 +875,7 @@ class ScrollView extends React.Component { * to the underlying scroll responder's methods. */ getScrollResponder: () => ScrollResponderType = () => { - // $FlowFixMe - overriding type to include ScrollResponder.Mixin + // $FlowFixMe[unclear-type] return ((this: any): ScrollResponderType); }; @@ -822,13 +883,13 @@ class ScrollView extends React.Component { return ReactNative.findNodeHandle(this._scrollViewRef); }; - getInnerViewNode(): ?number { + getInnerViewNode: () => ?number = () => { return ReactNative.findNodeHandle(this._innerViewRef); - } + }; - getInnerViewRef(): ?React.ElementRef { + getInnerViewRef: () => ?React.ElementRef = () => { return this._innerViewRef; - } + }; getNativeScrollRef: () => ?React.ElementRef> = () => { return this._scrollViewRef; @@ -882,11 +943,10 @@ class ScrollView extends React.Component { x = options.x; animated = options.animated; } - this._scrollResponder.scrollResponderScrollTo({ - x: x || 0, - y: y || 0, - animated: animated !== false, - }); + if (this._scrollViewRef == null) { + return; + } + Commands.scrollTo(this._scrollViewRef, x || 0, y || 0, animated !== false); }; /** @@ -902,9 +962,10 @@ class ScrollView extends React.Component { ) => { // Default to true const animated = (options && options.animated) !== false; - this._scrollResponder.scrollResponderScrollToEnd({ - animated: animated, - }); + if (this._scrollViewRef == null) { + return; + } + Commands.scrollToEnd(this._scrollViewRef, animated); }; /** @@ -913,7 +974,150 @@ class ScrollView extends React.Component { * @platform ios */ flashScrollIndicators: () => void = () => { - this._scrollResponder.scrollResponderFlashScrollIndicators(); + if (this._scrollViewRef == null) { + return; + } + Commands.flashScrollIndicators(this._scrollViewRef); + }; + + /** + * 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 {number} nodeHandle The TextInput node handle + * @param {number} additionalOffset The scroll view's bottom "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: number | React.ElementRef>, + additionalOffset?: number, + preventNegativeScrollOffset?: boolean, + ) => void = ( + nodeHandle: number | React.ElementRef>, + additionalOffset?: number, + preventNegativeScrollOffset?: boolean, + ) => { + this._additionalScrollOffset = additionalOffset || 0; + this._preventNegativeScrollOffset = !!preventNegativeScrollOffset; + + if (this._innerViewRef == null) { + return; + } + + if (typeof nodeHandle === 'number') { + UIManager.measureLayout( + nodeHandle, + ReactNative.findNodeHandle(this), + // $FlowFixMe[method-unbinding] added when improving typing for this parameters + this._textInputFocusError, + this._inputMeasureAndScrollToKeyboard, + ); + } else { + nodeHandle.measureLayout( + this._innerViewRef, + this._inputMeasureAndScrollToKeyboard, + // $FlowFixMe[method-unbinding] added when improving typing for this parameters + this._textInputFocusError, + ); + } + }; + + /** + * 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 + ) => void = ( + rect: {| + x: number, + y: number, + width: number, + height: number, + animated?: boolean, + |}, + animated?: boolean, // deprecated, put this inside the rect argument instead + ) => { + invariant(Platform.OS === 'ios', 'zoomToRect is not implemented'); + if ('animated' in rect) { + this._animated = rect.animated; + delete rect.animated; + } else if (typeof animated !== 'undefined') { + console.warn( + '`scrollResponderZoomTo` `animated` argument is deprecated. Use `options.animated` instead', + ); + } + + if (this._scrollViewRef == null) { + return; + } + Commands.zoomToRect(this._scrollViewRef, rect, animated !== false); + }; + + _textInputFocusError() { + console.warn('Error measuring text field.'); + } + + /** + * 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. + */ + _inputMeasureAndScrollToKeyboard: ( + left: number, + top: number, + width: number, + height: number, + ) => void = (left: number, top: number, width: number, height: number) => { + let keyboardScreenY = Dimensions.get('window').height; + + const scrollTextInputIntoVisibleRect = () => { + if (this._keyboardWillOpenTo != null) { + 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 === true) { + scrollOffsetY = Math.max(0, scrollOffsetY); + } + this.scrollTo({x: 0, y: scrollOffsetY, animated: true}); + + this._additionalScrollOffset = 0; + this._preventNegativeScrollOffset = false; + }; + + if (this._keyboardWillOpenTo == null) { + // `_keyboardWillOpenTo` is set inside `scrollResponderKeyboardWillShow` which + // is not guaranteed to be called before `_inputMeasureAndScrollToKeyboard` but native has already scheduled it. + // In case it was not called before `_inputMeasureAndScrollToKeyboard`, we postpone scrolling to + // text input. + setTimeout(() => { + scrollTextInputIntoVisibleRect(); + }, 0); + } else { + scrollTextInputIntoVisibleRect(); + } }; _getKeyForIndex(index, childArray) { @@ -1072,14 +1276,12 @@ class ScrollView extends React.Component { } } if (Platform.OS === 'android') { - if ( - this.props.keyboardDismissMode === 'on-drag' && - this.state.isTouching - ) { + if (this.props.keyboardDismissMode === 'on-drag' && this._isTouching) { dismissKeyboard(); } } - this._scrollResponder.scrollResponderHandleScroll(e); + this._observedScrollSinceBecomingResponder = true; + this.props.onScroll && this.props.onScroll(e); }; _handleLayout = (e: LayoutEvent) => { @@ -1107,31 +1309,395 @@ class ScrollView extends React.Component { }, }); - render(): React.Node | React.Element { - let ScrollViewClass; - let ScrollContentContainerViewClass; - if (Platform.OS === 'android') { - if (this.props.horizontal === true) { - ScrollViewClass = AndroidHorizontalScrollView; - ScrollContentContainerViewClass = AndroidHorizontalScrollContentView; - } else { - ScrollViewClass = AndroidScrollView; - ScrollContentContainerViewClass = View; + /** + * 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` module 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: KeyboardEvent) => void = ( + e: KeyboardEvent, + ) => { + this._keyboardWillOpenTo = e; + this.props.onKeyboardWillShow && this.props.onKeyboardWillShow(e); + }; + + scrollResponderKeyboardWillHide: (e: KeyboardEvent) => void = ( + e: KeyboardEvent, + ) => { + this._keyboardWillOpenTo = null; + this.props.onKeyboardWillHide && this.props.onKeyboardWillHide(e); + }; + + scrollResponderKeyboardDidShow: (e: KeyboardEvent) => void = ( + e: KeyboardEvent, + ) => { + // 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: KeyboardEvent) => void = ( + e: KeyboardEvent, + ) => { + this._keyboardWillOpenTo = null; + this.props.onKeyboardDidHide && this.props.onKeyboardDidHide(e); + }; + + /** + * Invoke this from an `onMomentumScrollBegin` event. + */ + _handleMomentumScrollBegin: (e: ScrollEvent) => void = (e: ScrollEvent) => { + this._lastMomentumScrollBeginTime = global.performance.now(); + this.props.onMomentumScrollBegin && this.props.onMomentumScrollBegin(e); + }; + + /** + * Invoke this from an `onMomentumScrollEnd` event. + */ + _handleMomentumScrollEnd: (e: ScrollEvent) => void = (e: ScrollEvent) => { + FrameRateLogger.endScroll(); + this._lastMomentumScrollEndTime = global.performance.now(); + this.props.onMomentumScrollEnd && this.props.onMomentumScrollEnd(e); + }; + + /** + * 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. + */ + _handleScrollBeginDrag: (e: ScrollEvent) => void = (e: ScrollEvent) => { + FrameRateLogger.beginScroll(); // TODO: track all scrolls after implementing onScrollEndAnimation + this.props.onScrollBeginDrag && this.props.onScrollBeginDrag(e); + }; + + /** + * Invoke this from an `onScrollEndDrag` event. + */ + _handleScrollEndDrag: (e: ScrollEvent) => void = (e: ScrollEvent) => { + const {velocity} = e.nativeEvent; + // - If we are animating, then this is a "drag" that is stopping the scrollview and momentum end + // will fire. + // - If velocity is non-zero, then the interaction will stop when momentum scroll ends or + // another drag starts and ends. + // - If we don't get velocity, better to stop the interaction twice than not stop it. + if ( + !this._isAnimating() && + (!velocity || (velocity.x === 0 && velocity.y === 0)) + ) { + FrameRateLogger.endScroll(); + } + this.props.onScrollEndDrag && this.props.onScrollEndDrag(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. + */ + _isAnimating: () => boolean = () => { + const now = global.performance.now(); + const timeSinceLastMomentumScrollEnd = + now - this._lastMomentumScrollEndTime; + const isAnimating = + timeSinceLastMomentumScrollEnd < IS_ANIMATING_TOUCH_START_THRESHOLD_MS || + this._lastMomentumScrollEndTime < this._lastMomentumScrollBeginTime; + return isAnimating; + }; + + /** + * Invoke this from an `onResponderGrant` event. + */ + _handleResponderGrant: (e: PressEvent) => void = (e: PressEvent) => { + this._observedScrollSinceBecomingResponder = false; + this.props.onResponderGrant && this.props.onResponderGrant(e); + this._becameResponderWhileAnimating = this._isAnimating(); + }; + + /** + * 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. + */ + _handleResponderReject: () => void = () => {}; + + /** + * Invoke this from an `onResponderRelease` event. + */ + _handleResponderRelease: (e: PressEvent) => void = (e: PressEvent) => { + this._isTouching = e.nativeEvent.touches.length !== 0; + this.props.onResponderRelease && this.props.onResponderRelease(e); + + if (typeof e.target === 'number') { + if (__DEV__) { + console.error( + 'Did not expect event target to be a number. Should have been a native component', + ); } - } else { - ScrollViewClass = RCTScrollView; - ScrollContentContainerViewClass = RCTScrollContentView; + + return; } - invariant( - ScrollViewClass !== undefined, - 'ScrollViewClass must not be undefined', - ); + // By default scroll views will unfocus a textField + // if another touch occurs outside of it + const currentlyFocusedTextInput = TextInputState.currentlyFocusedInput(); + if ( + this.props.keyboardShouldPersistTaps !== true && + this.props.keyboardShouldPersistTaps !== 'always' && + this._keyboardIsDismissible() && + e.target !== currentlyFocusedTextInput && + !this._observedScrollSinceBecomingResponder && + !this._becameResponderWhileAnimating + ) { + TextInputState.blurTextInput(currentlyFocusedTextInput); + } + }; - invariant( - ScrollContentContainerViewClass !== undefined, - 'ScrollContentContainerViewClass must not be undefined', - ); + /** + * 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. + */ + _handleResponderTerminationRequest: () => boolean = () => { + return !this._observedScrollSinceBecomingResponder; + }; + + /** + * Invoke this from an `onScroll` event. + */ + _handleScrollShouldSetResponder: () => boolean = () => { + // Allow any event touch pass through if the default pan responder is disabled + if (this.props.disableScrollViewPanResponder === true) { + return false; + } + 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. + * + */ + _handleStartShouldSetResponder: (e: PressEvent) => boolean = ( + e: PressEvent, + ) => { + // Allow any event touch pass through if the default pan responder is disabled + if (this.props.disableScrollViewPanResponder === true) { + return false; + } + + const currentlyFocusedInput = TextInputState.currentlyFocusedInput(); + + if ( + this.props.keyboardShouldPersistTaps === 'handled' && + this._keyboardIsDismissible() && + e.target !== currentlyFocusedInput + ) { + return true; + } + 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 a text input, while the keyboard is + * up (which should dismiss the keyboard). + * + * Invoke this from an `onStartShouldSetResponderCapture` event. + */ + _handleStartShouldSetResponderCapture: (e: PressEvent) => boolean = ( + e: PressEvent, + ) => { + // The scroll view should receive taps instead of its descendants if: + // * it is already animating/decelerating + if (this._isAnimating()) { + return true; + } + + // Allow any event touch pass through if the default pan responder is disabled + if (this.props.disableScrollViewPanResponder === true) { + return false; + } + + // * the keyboard is up, keyboardShouldPersistTaps is 'never' (the default), + // and a new touch starts with a non-textinput target (in which case the + // first tap should be sent to the scroll view and dismiss the keyboard, + // then the second tap goes to the actual interior view) + const {keyboardShouldPersistTaps} = this.props; + const keyboardNeverPersistTaps = + !keyboardShouldPersistTaps || keyboardShouldPersistTaps === 'never'; + + if (typeof e.target === 'number') { + if (__DEV__) { + console.error( + 'Did not expect event target to be a number. Should have been a native component', + ); + } + + return false; + } + + if ( + keyboardNeverPersistTaps && + this._keyboardIsDismissible() && + e.target != null && + !TextInputState.isTextInput(e.target) + ) { + return true; + } + + return false; + }; + + /** + * Do we consider there to be a dismissible soft-keyboard open? + */ + _keyboardIsDismissible: () => boolean = () => { + const currentlyFocusedInput = TextInputState.currentlyFocusedInput(); + + // We cannot dismiss the keyboard without an input to blur, even if a soft + // keyboard is open (e.g. when keyboard is open due to a native component + // not participating in TextInputState). It's also possible that the + // currently focused input isn't a TextInput (such as by calling ref.focus + // on a non-TextInput). + const hasFocusedTextInput = + currentlyFocusedInput != null && + TextInputState.isTextInput(currentlyFocusedInput); + + // Even if an input is focused, we may not have a keyboard to dismiss. E.g + // when using a physical keyboard. Ensure we have an event for an opened + // keyboard, except on Android where setting windowSoftInputMode to + // adjustNone leads to missing keyboard events. + const softKeyboardMayBeOpen = + this._keyboardWillOpenTo != null || Platform.OS === 'android'; + + return hasFocusedTextInput && softKeyboardMayBeOpen; + }; + + /** + * Invoke this from an `onTouchEnd` event. + * + * @param {PressEvent} e Event. + */ + _handleTouchEnd: (e: PressEvent) => void = (e: PressEvent) => { + const nativeEvent = e.nativeEvent; + this._isTouching = nativeEvent.touches.length !== 0; + this.props.onTouchEnd && this.props.onTouchEnd(e); + }; + + /** + * Invoke this from an `onTouchCancel` event. + * + * @param {PressEvent} e Event. + */ + _handleTouchCancel: (e: PressEvent) => void = (e: PressEvent) => { + this._isTouching = false; + this.props.onTouchCancel && this.props.onTouchCancel(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 {PressEvent} e Touch Start event. + */ + _handleTouchStart: (e: PressEvent) => void = (e: PressEvent) => { + 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 {PressEvent} e Touch Start event. + */ + _handleTouchMove: (e: PressEvent) => void = (e: PressEvent) => { + this.props.onTouchMove && this.props.onTouchMove(e); + }; + + render(): React.Node | React.Element { + const [NativeDirectionalScrollView, NativeDirectionalScrollContentView] = + this.props.horizontal === true + ? NativeHorizontalScrollViewTuple + : NativeVerticalScrollViewTuple; const contentContainerStyle = [ this.props.horizontal === true && styles.contentContainerHorizontal, @@ -1150,12 +1716,12 @@ class ScrollView extends React.Component { ); } - let contentSizeChangeProps = {}; - if (this.props.onContentSizeChange) { - contentSizeChangeProps = { - onLayout: this._handleContentOnLayout, - }; - } + const contentSizeChangeProps = + this.props.onContentSizeChange == null + ? null + : { + onLayout: this._handleContentOnLayout, + }; const {stickyHeaderIndices} = this.props; let children = this.props.children; @@ -1181,6 +1747,7 @@ class ScrollView extends React.Component { onLayout={event => this._onStickyHeaderLayout(index, event, key)} scrollAnimatedValue={this._scrollAnimatedValue} inverted={this.props.invertStickyHeaders} + hiddenOnScroll={this.props.stickyHeaderHiddenOnScroll} scrollViewHeight={this.state.layoutHeight}> {child} @@ -1201,10 +1768,7 @@ class ScrollView extends React.Component { Array.isArray(stickyHeaderIndices) && stickyHeaderIndices.length > 0; const contentContainer = ( - /* $FlowFixMe(>=0.112.0 site=react_native_fb) This comment suppresses an - * error found when Flow v0.112 was deployed. To see the error, delete - * this comment and run Flow. */ - { key={this.state.contentKey} // TODO(macOS GH#774) collapsable={false}> {children} - + ); const alwaysBounceHorizontal = @@ -1239,7 +1803,7 @@ class ScrollView extends React.Component { ...this.props, alwaysBounceHorizontal, alwaysBounceVertical, - style: [baseStyle, this.props.style], + style: StyleSheet.compose(baseStyle, this.props.style), // Override the onContentSizeChange from props, since this event can // bubble up from TextInputs onContentSizeChange: null, @@ -1247,33 +1811,23 @@ class ScrollView extends React.Component { onPreferredScrollerStyleDidChange: this ._handlePreferredScrollerStyleDidChange, // TODO(macOS GH#774) onLayout: this._handleLayout, - onMomentumScrollBegin: this._scrollResponder - .scrollResponderHandleMomentumScrollBegin, - onMomentumScrollEnd: this._scrollResponder - .scrollResponderHandleMomentumScrollEnd, - onResponderGrant: this._scrollResponder - .scrollResponderHandleResponderGrant, - onResponderReject: this._scrollResponder - .scrollResponderHandleResponderReject, - onResponderRelease: this._scrollResponder - .scrollResponderHandleResponderRelease, - onResponderTerminationRequest: this._scrollResponder - .scrollResponderHandleTerminationRequest, - onScrollBeginDrag: this._scrollResponder - .scrollResponderHandleScrollBeginDrag, - onScrollEndDrag: this._scrollResponder.scrollResponderHandleScrollEndDrag, - onScrollShouldSetResponder: this._scrollResponder - .scrollResponderHandleScrollShouldSetResponder, - onStartShouldSetResponder: this._scrollResponder - .scrollResponderHandleStartShouldSetResponder, - onStartShouldSetResponderCapture: this._scrollResponder - .scrollResponderHandleStartShouldSetResponderCapture, - onTouchEnd: this._scrollResponder.scrollResponderHandleTouchEnd, - onTouchMove: this._scrollResponder.scrollResponderHandleTouchMove, - onTouchStart: this._scrollResponder.scrollResponderHandleTouchStart, - onTouchCancel: this._scrollResponder.scrollResponderHandleTouchCancel, + onMomentumScrollBegin: this._handleMomentumScrollBegin, + onMomentumScrollEnd: this._handleMomentumScrollEnd, + onResponderGrant: this._handleResponderGrant, + onResponderReject: this._handleResponderReject, + onResponderRelease: this._handleResponderRelease, + onResponderTerminationRequest: this._handleResponderTerminationRequest, + onScrollBeginDrag: this._handleScrollBeginDrag, + onScrollEndDrag: this._handleScrollEndDrag, + onScrollShouldSetResponder: this._handleScrollShouldSetResponder, + onStartShouldSetResponder: this._handleStartShouldSetResponder, + onStartShouldSetResponderCapture: this + ._handleStartShouldSetResponderCapture, + onTouchEnd: this._handleTouchEnd, + onTouchMove: this._handleTouchMove, + onTouchStart: this._handleTouchStart, + onTouchCancel: this._handleTouchCancel, onScroll: this._handleScroll, - scrollBarThumbImage: resolveAssetSource(this.props.scrollBarThumbImage), scrollEventThrottle: hasStickyHeaders ? 1 : this.props.scrollEventThrottle, @@ -1316,13 +1870,10 @@ class ScrollView extends React.Component { if (Platform.OS === 'ios') { // On iOS the RefreshControl is a child of the ScrollView. return ( - /* $FlowFixMe(>=0.117.0 site=react_native_fb) This comment suppresses - * an error found when Flow v0.117 was deployed. To see the error, - * delete this comment and run Flow. */ - + {refreshControl} {contentContainer} - + ); } else if (Platform.OS === 'android') { // On Android wrap the ScrollView with a AndroidSwipeRefreshLayout. @@ -1333,20 +1884,20 @@ class ScrollView extends React.Component { const {outer, inner} = splitLayoutProps(flattenStyle(props.style)); return React.cloneElement( refreshControl, - {style: [baseStyle, outer]}, - {contentContainer} - , + , ); } } return ( - + {contentContainer} - + ); } } @@ -1375,7 +1926,7 @@ function Wrapper(props, ref) { Wrapper.displayName = 'ScrollView'; const ForwardedScrollView = React.forwardRef(Wrapper); -// $FlowFixMe Add static context to ForwardedScrollView +// $FlowFixMe[prop-missing] Add static context to ForwardedScrollView ForwardedScrollView.Context = ScrollViewContext; ForwardedScrollView.displayName = 'ScrollView'; diff --git a/Libraries/Components/ScrollView/ScrollViewContext.js b/Libraries/Components/ScrollView/ScrollViewContext.js index f774483912d928..0b5e9ac40de003 100644 --- a/Libraries/Components/ScrollView/ScrollViewContext.js +++ b/Libraries/Components/ScrollView/ScrollViewContext.js @@ -8,14 +8,14 @@ * @format */ -'use strict'; - import * as React from 'react'; type Value = {horizontal: boolean} | null; const ScrollViewContext: React.Context = React.createContext(null); - +if (__DEV__) { + ScrollViewContext.displayName = 'ScrollViewContext'; +} export default ScrollViewContext; export const HORIZONTAL: Value = Object.freeze({horizontal: true}); diff --git a/Libraries/Components/ScrollView/ScrollViewNativeComponent.js b/Libraries/Components/ScrollView/ScrollViewNativeComponent.js index 1e1100f0cc3165..cdfc8ff450f3c3 100644 --- a/Libraries/Components/ScrollView/ScrollViewNativeComponent.js +++ b/Libraries/Components/ScrollView/ScrollViewNativeComponent.js @@ -4,29 +4,83 @@ * 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 - * @flow */ -'use strict'; +import type {ScrollViewNativeProps as Props} from './ScrollViewNativeComponentType'; +import type {HostComponent} from '../../Renderer/shims/ReactNativeTypes'; +import * as NativeComponentRegistry from '../../NativeComponent/NativeComponentRegistry'; -import registerGeneratedViewConfig from '../../Utilities/registerGeneratedViewConfig'; -import requireNativeComponent from '../../ReactNative/requireNativeComponent'; -import ScrollViewViewConfig from './ScrollViewViewConfig'; +const ScrollViewNativeComponent: HostComponent = NativeComponentRegistry.get( + 'RCTScrollView', + () => ({ + uiViewClassName: 'RCTScrollView', + bubblingEventTypes: {}, + directEventTypes: { + topScrollToTop: { + registrationName: 'onScrollToTop', + }, + }, + validAttributes: { + alwaysBounceHorizontal: true, + alwaysBounceVertical: true, + automaticallyAdjustContentInsets: true, + automaticallyAdjustsScrollIndicatorInsets: true, + bounces: true, + bouncesZoom: true, + canCancelContentTouches: true, + centerContent: true, + contentInset: { + diff: require('../../Utilities/differ/pointsDiffer'), + }, + contentOffset: { + diff: require('../../Utilities/differ/pointsDiffer'), + }, + contentInsetAdjustmentBehavior: true, + decelerationRate: true, + directionalLockEnabled: true, + disableIntervalMomentum: true, + endFillColor: { + process: require('../../StyleSheet/processColor'), + }, + fadingEdgeLength: true, + indicatorStyle: true, + inverted: true, + keyboardDismissMode: true, + maintainVisibleContentPosition: true, + maximumZoomScale: true, + minimumZoomScale: true, + nestedScrollEnabled: true, + onMomentumScrollBegin: true, + onMomentumScrollEnd: true, + onScroll: true, + onScrollBeginDrag: true, + onScrollEndDrag: true, + onScrollToTop: true, + overScrollMode: true, + pagingEnabled: true, + persistentScrollbar: true, + pinchGestureEnabled: true, + scrollEnabled: true, + scrollEventThrottle: true, + scrollIndicatorInsets: { + diff: require('../../Utilities/differ/pointsDiffer'), + }, + scrollPerfTag: true, + scrollToOverflowEnabled: true, + scrollsToTop: true, + sendMomentumEvents: true, + showsHorizontalScrollIndicator: true, + showsVerticalScrollIndicator: true, + snapToAlignment: true, + snapToEnd: true, + snapToInterval: true, + snapToOffsets: true, + snapToStart: true, + zoomScale: true, + }, + }), +); -import type { - ScrollViewNativeProps, - ScrollViewNativeComponentType, -} from './ScrollViewNativeComponentType'; - -let ScrollViewNativeComponent; -if (global.RN$Bridgeless) { - registerGeneratedViewConfig('RCTScrollView', ScrollViewViewConfig); - ScrollViewNativeComponent = 'RCTScrollView'; -} else { - ScrollViewNativeComponent = requireNativeComponent( - 'RCTScrollView', - ); -} - -export default ((ScrollViewNativeComponent: any): ScrollViewNativeComponentType); +export default ScrollViewNativeComponent; diff --git a/Libraries/Components/ScrollView/ScrollViewNativeComponentType.js b/Libraries/Components/ScrollView/ScrollViewNativeComponentType.js index a6366e8b62b347..24e0c70f442787 100644 --- a/Libraries/Components/ScrollView/ScrollViewNativeComponentType.js +++ b/Libraries/Components/ScrollView/ScrollViewNativeComponentType.js @@ -4,18 +4,13 @@ * 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 - * @flow */ 'use strict'; -import type {HostComponent} from '../../Renderer/shims/ReactNativeTypes'; import type {ViewProps} from '../View/ViewPropTypes'; -import type { - ViewStyleProp, - DangerouslyImpreciseStyle, -} from '../../StyleSheet/StyleSheet'; import type {ColorValue} from '../../StyleSheet/StyleSheet'; import type {EdgeInsetsProp} from '../../StyleSheet/EdgeInsetsPropType'; import type {ScrollEvent} from '../../Types/CoreEventTypes'; @@ -26,6 +21,7 @@ export type ScrollViewNativeProps = $ReadOnly<{ alwaysBounceHorizontal?: ?boolean, alwaysBounceVertical?: ?boolean, automaticallyAdjustContentInsets?: ?boolean, + automaticallyAdjustsScrollIndicatorInsets?: ?boolean, bounces?: ?boolean, bouncesZoom?: ?boolean, canCancelContentTouches?: ?boolean, @@ -45,10 +41,10 @@ export type ScrollViewNativeProps = $ReadOnly<{ fadingEdgeLength?: ?number, indicatorStyle?: ?('default' | 'black' | 'white'), keyboardDismissMode?: ?('none' | 'on-drag' | 'interactive'), - maintainVisibleContentPosition?: ?$ReadOnly<{| + maintainVisibleContentPosition?: ?$ReadOnly<{ minIndexForVisible: number, autoscrollToTopThreshold?: ?number, - |}>, + }>, maximumZoomScale?: ?number, minimumZoomScale?: ?number, nestedScrollEnabled?: ?boolean, @@ -78,9 +74,6 @@ export type ScrollViewNativeProps = $ReadOnly<{ snapToStart?: ?boolean, zoomScale?: ?number, // Overrides - style?: {...ViewStyleProp, ...} | DangerouslyImpreciseStyle, - onResponderGrant?: ?(e: any) => void | boolean, + onResponderGrant?: ?(e: $FlowFixMe) => void | boolean, ... }>; - -export type ScrollViewNativeComponentType = HostComponent; diff --git a/Libraries/Components/ScrollView/ScrollViewStickyHeader.js b/Libraries/Components/ScrollView/ScrollViewStickyHeader.js index a71b9d56020386..f530be611602e1 100644 --- a/Libraries/Components/ScrollView/ScrollViewStickyHeader.js +++ b/Libraries/Components/ScrollView/ScrollViewStickyHeader.js @@ -4,289 +4,303 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @flow + * @flow strict-local * @format */ -'use strict'; - -import AnimatedImplementation from '../../Animated/AnimatedImplementation'; -import * as React from 'react'; -import StyleSheet from '../../StyleSheet/StyleSheet'; -import View from '../View/View'; -import Platform from '../../Utilities/Platform'; - import type {LayoutEvent} from '../../Types/CoreEventTypes'; +import setAndForwardRef from 'react-native/Libraries/Utilities/setAndForwardRef'; +import Platform from '../../Utilities/Platform'; +import StyleSheet from '../../StyleSheet/StyleSheet'; +import Animated from '../../Animated/Animated'; +import * as React from 'react'; +import {useEffect, useMemo, useRef, useCallback} from 'react'; -const AnimatedView = AnimatedImplementation.createAnimatedComponent(View); +const AnimatedView = Animated.View; -export type Props = { - children?: React.Element, +export type Props = $ReadOnly<{ + children?: React.Element<$FlowFixMe>, nextHeaderLayoutY: ?number, onLayout: (event: LayoutEvent) => void, - scrollAnimatedValue: AnimatedImplementation.Value, + scrollAnimatedValue: Animated.Value, // Will cause sticky headers to stick at the bottom of the ScrollView instead // of the top. inverted: ?boolean, // The height of the parent ScrollView. Currently only set when inverted. scrollViewHeight: ?number, nativeID?: ?string, - ... -}; - -type State = { - measured: boolean, - layoutY: number, - layoutHeight: number, - nextHeaderLayoutY: ?number, - translateY: ?number, - ... -}; + hiddenOnScroll?: ?boolean, +}>; -class ScrollViewStickyHeader extends React.Component { - state: State = { - measured: false, - layoutY: 0, - layoutHeight: 0, - nextHeaderLayoutY: this.props.nextHeaderLayoutY, - translateY: null, - }; +const ScrollViewStickyHeaderWithForwardedRef: React.AbstractComponent< + Props, + $ReadOnly<{ + setNextHeaderY: number => void, + ...$Exact>, + }>, +> = React.forwardRef(function ScrollViewStickyHeader(props, forwardedRef) { + const { + inverted, + scrollViewHeight, + hiddenOnScroll, + scrollAnimatedValue, + nextHeaderLayoutY: _nextHeaderLayoutY, + } = props; - _translateY: ?AnimatedImplementation.Interpolation = null; - _shouldRecreateTranslateY: boolean = true; - _haveReceivedInitialZeroTranslateY: boolean = true; - _ref: any; // TODO T53738161: flow type this, and the whole file + const [measured, setMeasured] = React.useState(false); + const [layoutY, setLayoutY] = React.useState(0); + const [layoutHeight, setLayoutHeight] = React.useState(0); + const [translateY, setTranslateY] = React.useState(null); + const [nextHeaderLayoutY, setNextHeaderLayoutY] = React.useState( + _nextHeaderLayoutY, + ); + const [isFabric, setIsFabric] = React.useState(false); - // Fabric-only: - _timer: ?TimeoutID; - _animatedValueListenerId: string; - _animatedValueListener: (valueObject: $ReadOnly<{|value: number|}>) => void; - _debounceTimeout: number = Platform.OS === 'android' ? 15 : 64; + const componentRef = React.useRef>(); + const _setNativeRef = setAndForwardRef({ + getForwardedRef: () => forwardedRef, + setLocalRef: ref => { + componentRef.current = ref; + if (ref) { + ref.setNextHeaderY = value => { + setNextHeaderLayoutY(value); + }; + setIsFabric( + !!( + // An internal transform mangles variables with leading "_" as private. + // eslint-disable-next-line dot-notation + ref['_internalInstanceHandle']?.stateNode?.canonical + ), + ); + } + }, + }); - setNextHeaderY(y: number) { - this.setState({nextHeaderLayoutY: y}); - } + const offset = useMemo( + () => + hiddenOnScroll === true + ? Animated.diffClamp( + scrollAnimatedValue + .interpolate({ + extrapolateLeft: 'clamp', + inputRange: [layoutY, layoutY + 1], + outputRange: ([0, 1]: Array), + }) + .interpolate({ + inputRange: [0, 1], + outputRange: ([0, -1]: Array), + }), + -layoutHeight, + 0, + ) + : null, + [scrollAnimatedValue, layoutHeight, layoutY, hiddenOnScroll], + ); - UNSAFE_componentWillReceiveProps(nextProps: Props) { - if ( - nextProps.scrollViewHeight !== this.props.scrollViewHeight || - nextProps.scrollAnimatedValue !== this.props.scrollAnimatedValue || - nextProps.inverted !== this.props.inverted - ) { - this._shouldRecreateTranslateY = true; - } - } + const [ + animatedTranslateY, + setAnimatedTranslateY, + ] = React.useState(() => { + const inputRange: Array = [-1, 0]; + const outputRange: Array = [0, 0]; + const initialTranslateY: Animated.Interpolation = scrollAnimatedValue.interpolate( + { + inputRange, + outputRange, + }, + ); - updateTranslateListener( - translateY: AnimatedImplementation.Interpolation, - isFabric: boolean, - ) { - if (this._translateY != null && this._animatedValueListenerId != null) { - this._translateY.removeListener(this._animatedValueListenerId); + if (offset != null) { + return Animated.add(initialTranslateY, offset); } + return initialTranslateY; + }); - this._translateY = translateY; - this._shouldRecreateTranslateY = false; + const _haveReceivedInitialZeroTranslateY = useRef(true); + const _timer = useRef(null); - if (!isFabric) { - return; + useEffect(() => { + if (translateY !== 0 && translateY != null) { + _haveReceivedInitialZeroTranslateY.current = false; } + }, [translateY]); - if (!this._animatedValueListener) { - // This is called whenever the (Interpolated) Animated Value - // updates, which is several times per frame during scrolling. - // To ensure that the Fabric ShadowTree has the most recent - // translate style of this node, we debounce the value and then - // pass it through to the underlying node during render. - // This is: - // 1. Only an issue in Fabric. - // 2. Worse in Android than iOS. In Android, but not iOS, you - // can touch and move your finger slightly and still trigger - // a "tap" event. In iOS, moving will cancel the tap in - // both Fabric and non-Fabric. On Android when you move - // your finger, the hit-detection moves from the Android - // platform to JS, so we need the ShadowTree to have knowledge - // of the current position. - this._animatedValueListener = ({value}) => { - // When the AnimatedInterpolation is recreated, it always initializes - // to a value of zero and emits a value change of 0 to its listeners. - if (value === 0 && !this._haveReceivedInitialZeroTranslateY) { - this._haveReceivedInitialZeroTranslateY = true; - return; - } - if (this._timer) { - clearTimeout(this._timer); + // This is called whenever the (Interpolated) Animated Value + // updates, which is several times per frame during scrolling. + // To ensure that the Fabric ShadowTree has the most recent + // translate style of this node, we debounce the value and then + // pass it through to the underlying node during render. + // This is: + // 1. Only an issue in Fabric. + // 2. Worse in Android than iOS. In Android, but not iOS, you + // can touch and move your finger slightly and still trigger + // a "tap" event. In iOS, moving will cancel the tap in + // both Fabric and non-Fabric. On Android when you move + // your finger, the hit-detection moves from the Android + // platform to JS, so we need the ShadowTree to have knowledge + // of the current position. + const animatedValueListener = useCallback( + ({value}) => { + const _debounceTimeout: number = Platform.OS === 'android' ? 15 : 64; + // When the AnimatedInterpolation is recreated, it always initializes + // to a value of zero and emits a value change of 0 to its listeners. + if (value === 0 && !_haveReceivedInitialZeroTranslateY.current) { + _haveReceivedInitialZeroTranslateY.current = true; + return; + } + if (_timer.current != null) { + clearTimeout(_timer.current); + } + _timer.current = setTimeout(() => { + if (value !== translateY) { + setTranslateY(value); } - this._timer = setTimeout(() => { - if (value !== this.state.translateY) { - this.setState({ - translateY: value, - }); - } - }, this._debounceTimeout); - }; - } - if (this.state.translateY !== 0 && this.state.translateY != null) { - this._haveReceivedInitialZeroTranslateY = false; - } - this._animatedValueListenerId = translateY.addListener( - this._animatedValueListener, - ); - } + }, _debounceTimeout); + }, + [translateY], + ); - _onLayout = event => { - const layoutY = event.nativeEvent.layout.y; - const layoutHeight = event.nativeEvent.layout.height; - const measured = true; + useEffect(() => { + const inputRange: Array = [-1, 0]; + const outputRange: Array = [0, 0]; - if ( - layoutY !== this.state.layoutY || - layoutHeight !== this.state.layoutHeight || - measured !== this.state.measured - ) { - this._shouldRecreateTranslateY = true; + if (measured) { + if (inverted === true) { + // The interpolation looks like: + // - Negative scroll: no translation + // - `stickStartPoint` is the point at which the header will start sticking. + // It is calculated using the ScrollView viewport height so it is a the bottom. + // - Headers that are in the initial viewport will never stick, `stickStartPoint` + // will be negative. + // - From 0 to `stickStartPoint` no translation. This will cause the header + // to scroll normally until it reaches the top of the scroll view. + // - From `stickStartPoint` to when the next header y hits the bottom edge of the header: translate + // equally to scroll. This will cause the header to stay at the top of the scroll view. + // - Past the collision with the next header y: no more translation. This will cause the + // header to continue scrolling up and make room for the next sticky header. + // In the case that there is no next header just translate equally to + // scroll indefinitely. + if (scrollViewHeight != null) { + const stickStartPoint = layoutY + layoutHeight - scrollViewHeight; + if (stickStartPoint > 0) { + inputRange.push(stickStartPoint); + outputRange.push(0); + inputRange.push(stickStartPoint + 1); + outputRange.push(1); + // If the next sticky header has not loaded yet (probably windowing) or is the last + // we can just keep it sticked forever. + const collisionPoint = + (nextHeaderLayoutY || 0) - layoutHeight - scrollViewHeight; + if (collisionPoint > stickStartPoint) { + inputRange.push(collisionPoint, collisionPoint + 1); + outputRange.push( + collisionPoint - stickStartPoint, + collisionPoint - stickStartPoint, + ); + } + } + } + } else { + // The interpolation looks like: + // - Negative scroll: no translation + // - From 0 to the y of the header: no translation. This will cause the header + // to scroll normally until it reaches the top of the scroll view. + // - From header y to when the next header y hits the bottom edge of the header: translate + // equally to scroll. This will cause the header to stay at the top of the scroll view. + // - Past the collision with the next header y: no more translation. This will cause the + // header to continue scrolling up and make room for the next sticky header. + // In the case that there is no next header just translate equally to + // scroll indefinitely. + inputRange.push(layoutY); + outputRange.push(0); + // If the next sticky header has not loaded yet (probably windowing) or is the last + // we can just keep it sticked forever. + const collisionPoint = (nextHeaderLayoutY || 0) - layoutHeight; + if (collisionPoint >= layoutY) { + inputRange.push(collisionPoint, collisionPoint + 1); + outputRange.push(collisionPoint - layoutY, collisionPoint - layoutY); + } else { + inputRange.push(layoutY + 1); + outputRange.push(1); + } + } } - this.setState({ - measured, - layoutY, - layoutHeight, + let newAnimatedTranslateY: Animated.Node = scrollAnimatedValue.interpolate({ + inputRange, + outputRange, }); - this.props.onLayout(event); - const child = React.Children.only(this.props.children); - if (child.props.onLayout) { - child.props.onLayout(event); + if (offset != null) { + newAnimatedTranslateY = Animated.add(newAnimatedTranslateY, offset); } - }; - - _setComponentRef = ref => { - this._ref = ref; - }; - render(): React.Node { - // Fabric Detection - const isFabric = !!( - // An internal transform mangles variables with leading "_" as private. - // eslint-disable-next-line dot-notation - (this._ref && this._ref['_internalInstanceHandle']?.stateNode?.canonical) - ); + // add the event listener + let animatedListenerId; + if (isFabric) { + animatedListenerId = newAnimatedTranslateY.addListener( + animatedValueListener, + ); + } - // Initially and in the case of updated props or layout, we - // recreate this interpolated value. Otherwise, we do not recreate - // when there are state changes. - if (this._shouldRecreateTranslateY) { - const {inverted, scrollViewHeight} = this.props; - const {measured, layoutHeight, layoutY, nextHeaderLayoutY} = this.state; - const inputRange: Array = [-1, 0]; - const outputRange: Array = [0, 0]; + setAnimatedTranslateY(newAnimatedTranslateY); - if (measured) { - if (inverted) { - // The interpolation looks like: - // - Negative scroll: no translation - // - `stickStartPoint` is the point at which the header will start sticking. - // It is calculated using the ScrollView viewport height so it is a the bottom. - // - Headers that are in the initial viewport will never stick, `stickStartPoint` - // will be negative. - // - From 0 to `stickStartPoint` no translation. This will cause the header - // to scroll normally until it reaches the top of the scroll view. - // - From `stickStartPoint` to when the next header y hits the bottom edge of the header: translate - // equally to scroll. This will cause the header to stay at the top of the scroll view. - // - Past the collision with the next header y: no more translation. This will cause the - // header to continue scrolling up and make room for the next sticky header. - // In the case that there is no next header just translate equally to - // scroll indefinitely. - if (scrollViewHeight != null) { - const stickStartPoint = layoutY + layoutHeight - scrollViewHeight; - if (stickStartPoint > 0) { - inputRange.push(stickStartPoint); - outputRange.push(0); - inputRange.push(stickStartPoint + 1); - outputRange.push(1); - // If the next sticky header has not loaded yet (probably windowing) or is the last - // we can just keep it sticked forever. - const collisionPoint = - (nextHeaderLayoutY || 0) - layoutHeight - scrollViewHeight; - if (collisionPoint > stickStartPoint) { - inputRange.push(collisionPoint, collisionPoint + 1); - outputRange.push( - collisionPoint - stickStartPoint, - collisionPoint - stickStartPoint, - ); - } - } - } - } else { - // The interpolation looks like: - // - Negative scroll: no translation - // - From 0 to the y of the header: no translation. This will cause the header - // to scroll normally until it reaches the top of the scroll view. - // - From header y to when the next header y hits the bottom edge of the header: translate - // equally to scroll. This will cause the header to stay at the top of the scroll view. - // - Past the collision with the next header y: no more translation. This will cause the - // header to continue scrolling up and make room for the next sticky header. - // In the case that there is no next header just translate equally to - // scroll indefinitely. - inputRange.push(layoutY); - outputRange.push(0); - // If the next sticky header has not loaded yet (probably windowing) or is the last - // we can just keep it sticked forever. - const collisionPoint = (nextHeaderLayoutY || 0) - layoutHeight; - if (collisionPoint >= layoutY) { - inputRange.push(collisionPoint, collisionPoint + 1); - outputRange.push( - collisionPoint - layoutY, - collisionPoint - layoutY, - ); - } else { - inputRange.push(layoutY + 1); - outputRange.push(1); - } - } + // clean up the event listener and timer + return () => { + if (animatedListenerId) { + newAnimatedTranslateY.removeListener(animatedListenerId); + } + if (_timer.current != null) { + clearTimeout(_timer.current); } + }; + }, [nextHeaderLayoutY, measured, layoutHeight, layoutY, scrollViewHeight, scrollAnimatedValue, inverted, offset, animatedValueListener, isFabric]); - this.updateTranslateListener( - this.props.scrollAnimatedValue.interpolate({ - inputRange, - outputRange, - }), - isFabric, - ); + const _onLayout = (event: LayoutEvent) => { + setLayoutY(event.nativeEvent.layout.y); + setLayoutHeight(event.nativeEvent.layout.height); + setMeasured(true); + + props.onLayout(event); + const child = React.Children.only(props.children); + if (child.props.onLayout) { + child.props.onLayout(event); } + }; - const child = React.Children.only(this.props.children); + const child = React.Children.only(props.children); - // TODO T68319535: remove this if NativeAnimated is rewritten for Fabric - const passthroughAnimatedPropExplicitValues = - isFabric && this.state.translateY != null - ? { - style: {transform: [{translateY: this.state.translateY}]}, - } - : null; + // TODO T68319535: remove this if NativeAnimated is rewritten for Fabric + const passthroughAnimatedPropExplicitValues = + isFabric && translateY != null + ? { + style: {transform: [{translateY: translateY}]}, + } + : null; - return ( - - {React.cloneElement(child, { - style: styles.fill, // We transfer the child style to the wrapper. - onLayout: undefined, // we call this manually through our this._onLayout - })} - - ); - } -} + return ( + /* $FlowFixMe[prop-missing] passthroughAnimatedPropExplicitValues isn't properly + included in the Animated.View flow type. */ + + {React.cloneElement(child, { + style: styles.fill, // We transfer the child style to the wrapper. + onLayout: undefined, // we call this manually through our this._onLayout + })} + + ); +}); const styles = StyleSheet.create({ header: { @@ -298,4 +312,4 @@ const styles = StyleSheet.create({ }, }); -module.exports = ScrollViewStickyHeader; +export default ScrollViewStickyHeaderWithForwardedRef; diff --git a/Libraries/Components/ScrollView/ScrollViewViewConfig.js b/Libraries/Components/ScrollView/ScrollViewViewConfig.js index b2a3a9246b343b..5b276d54fa0fe0 100644 --- a/Libraries/Components/ScrollView/ScrollViewViewConfig.js +++ b/Libraries/Components/ScrollView/ScrollViewViewConfig.js @@ -10,7 +10,7 @@ 'use strict'; -import type {GeneratedViewConfig} from '../../Utilities/registerGeneratedViewConfig'; +import type {PartialViewConfig} from '../../Renderer/shims/ReactNativeTypes'; const ScrollViewViewConfig = { uiViewClassName: 'RCTScrollView', @@ -24,19 +24,27 @@ const ScrollViewViewConfig = { alwaysBounceHorizontal: true, alwaysBounceVertical: true, automaticallyAdjustContentInsets: true, + automaticallyAdjustsScrollIndicatorInsets: true, bounces: true, bouncesZoom: true, canCancelContentTouches: true, centerContent: true, - contentInset: {diff: require('../../Utilities/differ/pointsDiffer')}, - contentOffset: {diff: require('../../Utilities/differ/pointsDiffer')}, + contentInset: { + diff: require('../../Utilities/differ/pointsDiffer'), + }, + contentOffset: { + diff: require('../../Utilities/differ/pointsDiffer'), + }, contentInsetAdjustmentBehavior: true, decelerationRate: true, directionalLockEnabled: true, disableIntervalMomentum: true, - endFillColor: {process: require('../../StyleSheet/processColor')}, + endFillColor: { + process: require('../../StyleSheet/processColor'), + }, fadingEdgeLength: true, indicatorStyle: true, + inverted: true, keyboardDismissMode: true, maintainVisibleContentPosition: true, maximumZoomScale: true, @@ -72,4 +80,4 @@ const ScrollViewViewConfig = { }, }; -module.exports = (ScrollViewViewConfig: GeneratedViewConfig); +module.exports = (ScrollViewViewConfig: PartialViewConfig); diff --git a/Libraries/Components/ScrollView/__tests__/ScrollView-test.js b/Libraries/Components/ScrollView/__tests__/ScrollView-test.js index fb7aa74a55703e..f2a7abe2c9f852 100644 --- a/Libraries/Components/ScrollView/__tests__/ScrollView-test.js +++ b/Libraries/Components/ScrollView/__tests__/ScrollView-test.js @@ -11,12 +11,12 @@ 'use strict'; -const React = require('react'); -const ScrollView = require('../ScrollView'); -const ReactNativeTestTools = require('../../../Utilities/ReactNativeTestTools'); -const ReactTestRenderer = require('react-test-renderer'); -const View = require('../../View/View'); -const Text = require('../../../Text/Text'); +import * as React from 'react'; +import ScrollView from '../ScrollView'; +import * as ReactNativeTestTools from '../../../Utilities/ReactNativeTestTools'; +import ReactTestRenderer from 'react-test-renderer'; +import View from '../../View/View'; +import Text from '../../../Text/Text'; describe('', () => { it('should render as expected', () => { @@ -48,4 +48,20 @@ describe('', () => { jest.fn().constructor, ); }); + it('getInnerViewRef for case where it returns a native view', () => { + jest.resetModules(); + jest.unmock('../ScrollView'); + + const scrollViewRef = React.createRef(null); + + ReactTestRenderer.create(); + + const innerViewRef = scrollViewRef.current.getInnerViewRef(); + + // This is checking if the ref acts like a host component. If we had an + // `isHostComponent(ref)` method, that would be preferred. + expect(innerViewRef.measure).toBeInstanceOf(jest.fn().constructor); + expect(innerViewRef.measureLayout).toBeInstanceOf(jest.fn().constructor); + expect(innerViewRef.measureInWindow).toBeInstanceOf(jest.fn().constructor); + }); }); diff --git a/Libraries/Components/ScrollView/__tests__/__snapshots__/ScrollView-test.js.snap b/Libraries/Components/ScrollView/__tests__/__snapshots__/ScrollView-test.js.snap index 47a15d64c9348e..6fb8cfb75c3c1b 100644 --- a/Libraries/Components/ScrollView/__tests__/__snapshots__/ScrollView-test.js.snap +++ b/Libraries/Components/ScrollView/__tests__/__snapshots__/ScrollView-test.js.snap @@ -36,21 +36,17 @@ exports[` should render as expected: should deep render when not m onTouchMove={[Function]} onTouchStart={[Function]} pagingEnabled={false} - scrollBarThumbImage={null} scrollViewRef={null} sendMomentumEvents={false} snapToEnd={true} snapToStart={true} style={ - Array [ - Object { - "flexDirection": "column", - "flexGrow": 1, - "flexShrink": 1, - "overflow": "scroll", - }, - undefined, - ] + Object { + "flexDirection": "column", + "flexGrow": 1, + "flexShrink": 1, + "overflow": "scroll", + } } > , /** @@ -29,6 +29,8 @@ type SegmentedControlIOSProps = $ReadOnly<{| selectedIndex?: ?number, /** * If false the user won't be able to interact with the control. + * + * The default value is true. */ enabled?: boolean, /** @@ -78,11 +80,6 @@ type Props = $ReadOnly<{| */ class SegmentedControlIOS extends React.Component { - static defaultProps = { - values: [], - enabled: true, - }; - _onChange = (event: SyntheticEvent) => { this.props.onChange && this.props.onChange(event); this.props.onValueChange && @@ -90,12 +87,21 @@ class SegmentedControlIOS extends React.Component { }; render() { - const {forwardedRef, onValueChange, style, ...props} = this.props; + const { + enabled, + forwardedRef, + onValueChange, + style, + values, + ...props + } = this.props; return ( ); @@ -117,7 +123,7 @@ const SegmentedControlIOSWithRef = React.forwardRef( }, ); -/* $FlowFixMe(>=0.89.0 site=react_native_ios_fb) This comment suppresses an - * error found when Flow v0.89 was deployed. To see the error, delete this - * comment and run Flow. */ +/* $FlowFixMe[cannot-resolve-name] (>=0.89.0 site=react_native_ios_fb) This + * comment suppresses an error found when Flow v0.89 was deployed. To see the + * error, delete this comment and run Flow. */ module.exports = (SegmentedControlIOSWithRef: NativeSegmentedControlIOS); diff --git a/Libraries/Components/SegmentedControlIOS/__tests__/SegmentedContolIOS-test.js b/Libraries/Components/SegmentedControlIOS/__tests__/SegmentedContolIOS-test.js new file mode 100644 index 00000000000000..605864bf697b88 --- /dev/null +++ b/Libraries/Components/SegmentedControlIOS/__tests__/SegmentedContolIOS-test.js @@ -0,0 +1,55 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @emails oncall+react_native + */ + +'use strict'; + +const React = require('react'); +const ReactTestRenderer = require('react-test-renderer'); + +const SegmentedControlIOS = require('../SegmentedControlIOS.ios'); + +describe('SegmentedControlIOS', () => { + it('renders the segmented control', () => { + const component = ReactTestRenderer.create(); + expect(component).not.toBeNull(); + }); + it('renders the segmented control with enabled default value', () => { + const component = ReactTestRenderer.create(); + expect(component.toTree().rendered.props.enabled).toBe(true); + expect(component).toMatchSnapshot(); + }); + it('renders the segmented control with enabled', () => { + const component = ReactTestRenderer.create( + , + ); + expect(component.toTree().rendered.props.enabled).toBe(true); + expect(component).toMatchSnapshot(); + }); + it('renders the segmented control with enabled set to false', () => { + const component = ReactTestRenderer.create( + , + ); + expect(component.toTree().rendered.props.enabled).toBe(false); + expect(component).toMatchSnapshot(); + }); + it('renders the segmented control with values default value', () => { + const component = ReactTestRenderer.create(); + expect(component.toTree().rendered.props.values).toEqual([]); + expect(component).toMatchSnapshot(); + }); + it('renders the segmented control with values', () => { + const values = ['One', 'Two']; + const component = ReactTestRenderer.create( + , + ); + expect(component.toTree().rendered.props.values).toBe(values); + expect(component).toMatchSnapshot(); + }); +}); diff --git a/Libraries/Components/SegmentedControlIOS/__tests__/__snapshots__/SegmentedContolIOS-test.js.snap b/Libraries/Components/SegmentedControlIOS/__tests__/__snapshots__/SegmentedContolIOS-test.js.snap new file mode 100644 index 00000000000000..30195d27c7f857 --- /dev/null +++ b/Libraries/Components/SegmentedControlIOS/__tests__/__snapshots__/SegmentedContolIOS-test.js.snap @@ -0,0 +1,86 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SegmentedControlIOS renders the segmented control with enabled 1`] = ` + +`; + +exports[`SegmentedControlIOS renders the segmented control with enabled default value 1`] = ` + +`; + +exports[`SegmentedControlIOS renders the segmented control with enabled set to false 1`] = ` + +`; + +exports[`SegmentedControlIOS renders the segmented control with values 1`] = ` + +`; + +exports[`SegmentedControlIOS renders the segmented control with values default value 1`] = ` + +`; diff --git a/Libraries/Components/Slider/Slider.js b/Libraries/Components/Slider/Slider.js index 784af7cc272750..1edf184c172ec9 100644 --- a/Libraries/Components/Slider/Slider.js +++ b/Libraries/Components/Slider/Slider.js @@ -8,18 +8,18 @@ * @flow strict-local */ -'use strict'; - -const Platform = require('../../Utilities/Platform'); +import * as React from 'react'; +import Platform from '../../Utilities/Platform'; import SliderNativeComponent from './SliderNativeComponent'; -const React = require('react'); -const StyleSheet = require('../../StyleSheet/StyleSheet'); +import StyleSheet, { + type ViewStyleProp, + type ColorValue, +} from '../../StyleSheet/StyleSheet'; import type {ImageSource} from '../../Image/ImageSource'; -import type {ViewStyleProp} from '../../StyleSheet/StyleSheet'; -import type {ColorValue} from '../../StyleSheet/StyleSheet'; import type {ViewProps} from '../View/ViewPropTypes'; import type {SyntheticEvent} from '../../Types/CoreEventTypes'; +import type {AccessibilityState} from '../View/ViewAccessibility'; type Event = SyntheticEvent< $ReadOnly<{| @@ -132,10 +132,18 @@ type Props = $ReadOnly<{| * Used to locate this view in UI automation tests. */ testID?: ?string, + + // [ TODO(macOS GH#774) /** * Specifies the tooltip. */ tooltip?: ?string, + // TODO(macOS GH#774) ] + + /** + Indicates to accessibility services that UI Component is in a specific State. + */ + accessibilityState?: ?AccessibilityState, |}>; /** @@ -205,7 +213,6 @@ const Slider = ( const style = StyleSheet.compose(styles.slider, props.style); const { - disabled = false, value = 0.5, minimumValue = 0, maximumValue = 1, @@ -235,9 +242,16 @@ const Slider = ( } : null; + const disabled = + props.disabled === true || props.accessibilityState?.disabled === true; + const accessibilityState = disabled + ? {...props.accessibilityState, disabled: true} + : props.accessibilityState; + return ( { static _propsStack = []; static _defaultProps = createStackEntry({ - animated: false, - showHideTransition: 'fade', backgroundColor: Platform.OS === 'android' ? NativeStatusBarManagerAndroid.getConstants() @@ -387,14 +384,6 @@ class StatusBar extends React.Component { return newEntry; } - static defaultProps: {| - animated: boolean, - showHideTransition: $TEMPORARY$string<'fade'>, - |} = { - animated: false, - showHideTransition: 'fade', - }; - _stackEntry = null; componentDidMount() { diff --git a/Libraries/Components/StatusBar/StatusBarIOS.js b/Libraries/Components/StatusBar/StatusBarIOS.js deleted file mode 100644 index ccc561103f187c..00000000000000 --- a/Libraries/Components/StatusBar/StatusBarIOS.js +++ /dev/null @@ -1,21 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its 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 strict'; - -import NativeEventEmitter from '../../EventEmitter/NativeEventEmitter'; -import NativeStatusBarManagerIOS from './NativeStatusBarManagerIOS'; - -/** - * Use `StatusBar` for mutating the status bar. - */ -class StatusBarIOS extends NativeEventEmitter {} - -module.exports = (new StatusBarIOS(NativeStatusBarManagerIOS): StatusBarIOS); diff --git a/Libraries/Components/StatusBar/__tests__/StatusBar-test.js b/Libraries/Components/StatusBar/__tests__/StatusBar-test.js new file mode 100644 index 00000000000000..a75d4fc1b0d31c --- /dev/null +++ b/Libraries/Components/StatusBar/__tests__/StatusBar-test.js @@ -0,0 +1,62 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @emails oncall+react_native + */ + +'use strict'; + +const React = require('react'); +const ReactTestRenderer = require('react-test-renderer'); + +const StatusBar = require('../StatusBar'); + +describe('StatusBar', () => { + it('renders the statusbar', () => { + const component = ReactTestRenderer.create(); + expect(component).not.toBeNull(); + }); + it('renders the statusbar animated enabled', () => { + const component = ReactTestRenderer.create(); + expect(component.toTree().props.animated).toBe(true); + }); + it('renders the statusbar with fade transition on hide', () => { + const component = ReactTestRenderer.create( `; + +exports[`TouchableHighlight with disabled state should be disabled when disabled is true 1`] = ` + + + +`; + +exports[`TouchableHighlight with disabled state should be disabled when disabled is true and accessibilityState is empty 1`] = ` + + + +`; + +exports[`TouchableHighlight with disabled state should disable button when accessibilityState is disabled 1`] = ` + + + +`; + +exports[`TouchableHighlight with disabled state should keep accessibilityState when disabled is true 1`] = ` + + + +`; + +exports[`TouchableHighlight with disabled state should overwrite accessibilityState with value of disabled prop 1`] = ` + + + +`; diff --git a/Libraries/Components/Touchable/__tests__/__snapshots__/TouchableNativeFeedback-test.js.snap b/Libraries/Components/Touchable/__tests__/__snapshots__/TouchableNativeFeedback-test.js.snap new file mode 100644 index 00000000000000..4c9f7b2c846993 --- /dev/null +++ b/Libraries/Components/Touchable/__tests__/__snapshots__/TouchableNativeFeedback-test.js.snap @@ -0,0 +1,141 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` should render as expected 1`] = ` + +`; + +exports[` should overwrite accessibilityState with value of disabled prop 1`] = ` + +`; + +exports[` should be disabled when disabled is true and accessibilityState is empty 1`] = ` + +`; + +exports[` should keep accessibilityState when disabled is true 1`] = ` + +`; + +exports[` should overwrite accessibilityState with value of disabled prop 1`] = ` + +`; + +exports[` should be disabled when disabled is true 1`] = ` + +`; + +exports[`TouchableWithoutFeedback renders correctly 1`] = ` + + Touchable + +`; diff --git a/Libraries/Components/Touchable/__tests__/__snapshots__/TouchableOpacity-test.js.snap b/Libraries/Components/Touchable/__tests__/__snapshots__/TouchableOpacity-test.js.snap new file mode 100644 index 00000000000000..d72c557b33faba --- /dev/null +++ b/Libraries/Components/Touchable/__tests__/__snapshots__/TouchableOpacity-test.js.snap @@ -0,0 +1,98 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TouchableOpacity renders correctly 1`] = ` + + + Touchable + + +`; + +exports[`TouchableOpacity renders in disabled state when a disabled prop is passed 1`] = ` + + + Touchable + + +`; + +exports[`TouchableOpacity renders in disabled state when a key disabled in accessibilityState is passed 1`] = ` + + + Touchable + + +`; diff --git a/Libraries/Components/Touchable/__tests__/__snapshots__/TouchableWithoutFeedback-test.js.snap b/Libraries/Components/Touchable/__tests__/__snapshots__/TouchableWithoutFeedback-test.js.snap new file mode 100644 index 00000000000000..9dfeb4e6d44ce5 --- /dev/null +++ b/Libraries/Components/Touchable/__tests__/__snapshots__/TouchableWithoutFeedback-test.js.snap @@ -0,0 +1,137 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TouchableWithoutFeedback renders correctly 1`] = ` + + Touchable + +`; + +exports[`TouchableWithoutFeedback with disabled state should be disabled when disabled is true 1`] = ` + +`; + +exports[`TouchableWithoutFeedback with disabled state should be disabled when disabled is true and accessibilityState is empty 1`] = ` + +`; + +exports[`TouchableWithoutFeedback with disabled state should disable button when accessibilityState is disabled 1`] = ` + +`; + +exports[`TouchableWithoutFeedback with disabled state should keep accessibilityState when disabled is true 1`] = ` + +`; + +exports[`TouchableWithoutFeedback with disabled state should overwrite accessibilityState with value of disabled prop 1`] = ` + +`; diff --git a/Libraries/Components/Touchable/ensurePositiveDelayProps.js b/Libraries/Components/Touchable/ensurePositiveDelayProps.js index 69dad5539faf2b..bd9b3028399cd0 100644 --- a/Libraries/Components/Touchable/ensurePositiveDelayProps.js +++ b/Libraries/Components/Touchable/ensurePositiveDelayProps.js @@ -9,8 +9,7 @@ */ 'use strict'; - -const invariant = require('invariant'); +import invariant from 'invariant'; const ensurePositiveDelayProps = function(props: any) { invariant( diff --git a/Libraries/Components/UnimplementedViews/UnimplementedNativeViewNativeComponent.js b/Libraries/Components/UnimplementedViews/UnimplementedNativeViewNativeComponent.js index 796650fefc53e3..2a2969905bc65d 100644 --- a/Libraries/Components/UnimplementedViews/UnimplementedNativeViewNativeComponent.js +++ b/Libraries/Components/UnimplementedViews/UnimplementedNativeViewNativeComponent.js @@ -8,8 +8,6 @@ * @flow strict-local */ -'use strict'; - import type {WithDefault} from '../../Types/CodegenTypes'; import type {ViewProps} from '../View/ViewPropTypes'; diff --git a/Libraries/Components/UnimplementedViews/UnimplementedView.js b/Libraries/Components/UnimplementedViews/UnimplementedView.js index 58804b7a772583..4bbb41060ff32f 100644 --- a/Libraries/Components/UnimplementedViews/UnimplementedView.js +++ b/Libraries/Components/UnimplementedViews/UnimplementedView.js @@ -9,9 +9,8 @@ */ 'use strict'; - -const React = require('react'); -const StyleSheet = require('../../StyleSheet/StyleSheet'); +import * as React from 'react'; +import StyleSheet from '../../StyleSheet/StyleSheet'; /** * Common implementation for a simple stubbed view. Simply applies the view's styles to the inner diff --git a/Libraries/Components/View/ReactNativeStyleAttributes.js b/Libraries/Components/View/ReactNativeStyleAttributes.js index ebd3c63df5614b..9f87352219ef2b 100644 --- a/Libraries/Components/View/ReactNativeStyleAttributes.js +++ b/Libraries/Components/View/ReactNativeStyleAttributes.js @@ -8,80 +8,145 @@ * @flow */ -'use strict'; +import type {AnyAttributeType} from '../../Renderer/shims/ReactNativeTypes'; +import processColor from '../../StyleSheet/processColor'; +import processTransform from '../../StyleSheet/processTransform'; +import sizesDiffer from '../../Utilities/differ/sizesDiffer'; -const DeprecatedImageStylePropTypes = require('../../DeprecatedPropTypes/DeprecatedImageStylePropTypes'); -const DeprecatedTextStylePropTypes = require('../../DeprecatedPropTypes/DeprecatedTextStylePropTypes'); -const DeprecatedViewStylePropTypes = require('../../DeprecatedPropTypes/DeprecatedViewStylePropTypes'); - -const processColor = require('../../StyleSheet/processColor'); -const processTransform = require('../../StyleSheet/processTransform'); -const sizesDiffer = require('../../Utilities/differ/sizesDiffer'); +const colorAttributes = {process: processColor}; -type ReturnBoolType = (V) => true; -type BoolifiedDeprecatedViewStylePropTypes = $ObjMap< - typeof DeprecatedViewStylePropTypes, - ReturnBoolType, ->; -type BoolifiedDeprecatedTextStylePropTypes = $ObjMapi< - typeof DeprecatedTextStylePropTypes, - ReturnBoolType, ->; -type BoolifiedDeprecatedImageStylePropTypes = $ObjMapi< - typeof DeprecatedImageStylePropTypes, - ReturnBoolType, ->; +const ReactNativeStyleAttributes: {[string]: AnyAttributeType, ...} = { + /** + * Layout + */ + alignContent: true, + alignItems: true, + alignSelf: true, + aspectRatio: true, + borderBottomWidth: true, + borderEndWidth: true, + borderLeftWidth: true, + borderRightWidth: true, + borderStartWidth: true, + borderTopWidth: true, + borderWidth: true, + bottom: true, + direction: true, + display: true, + end: true, + flex: true, + flexBasis: true, + flexDirection: true, + flexGrow: true, + flexShrink: true, + flexWrap: true, + height: true, + justifyContent: true, + left: true, + margin: true, + marginBottom: true, + marginEnd: true, + marginHorizontal: true, + marginLeft: true, + marginRight: true, + marginStart: true, + marginTop: true, + marginVertical: true, + maxHeight: true, + maxWidth: true, + minHeight: true, + minWidth: true, + overflow: true, + padding: true, + paddingBottom: true, + paddingEnd: true, + paddingHorizontal: true, + paddingLeft: true, + paddingRight: true, + paddingStart: true, + paddingTop: true, + paddingVertical: true, + position: true, + right: true, + start: true, + top: true, + width: true, + zIndex: true, -type StyleAttributesType = { - ...BoolifiedDeprecatedViewStylePropTypes, - ...BoolifiedDeprecatedTextStylePropTypes, - ...BoolifiedDeprecatedImageStylePropTypes, - transform: $ReadOnly<{|process: typeof processTransform|}> | true, - shadowOffset: $ReadOnly<{|diff: typeof sizesDiffer|}> | true, - backgroundColor: typeof colorAttributes | true, - borderBottomColor: typeof colorAttributes | true, - borderColor: typeof colorAttributes | true, - borderLeftColor: typeof colorAttributes | true, - borderRightColor: typeof colorAttributes | true, - borderTopColor: typeof colorAttributes | true, - borderStartColor: typeof colorAttributes | true, - borderEndColor: typeof colorAttributes | true, - color: typeof colorAttributes | true, - shadowColor: typeof colorAttributes | true, - textDecorationColor: typeof colorAttributes | true, - tintColor: typeof colorAttributes | true, - textShadowColor: typeof colorAttributes | true, - overlayColor: typeof colorAttributes | true, - ... -}; + /** + * Shadow + */ + elevation: true, + shadowColor: colorAttributes, + shadowOffset: {diff: sizesDiffer}, + shadowOpacity: true, + shadowRadius: true, -const ReactNativeStyleAttributes: StyleAttributesType = {}; + /** + * Transform + */ + decomposedMatrix: true, // @deprecated + rotation: true, // @deprecated + scaleX: true, // @deprecated + scaleY: true, // @deprecated + transform: {process: processTransform}, + transformMatrix: true, // @deprecated + translateX: true, // @deprecated + translateY: true, // @deprecated -for (const attributeName of Object.keys({ - ...DeprecatedViewStylePropTypes, - ...DeprecatedTextStylePropTypes, - ...DeprecatedImageStylePropTypes, -})) { - ReactNativeStyleAttributes[attributeName] = true; -} + /** + * View + */ + backfaceVisibility: true, + backgroundColor: colorAttributes, + borderBottomColor: colorAttributes, + borderBottomEndRadius: true, + borderBottomLeftRadius: true, + borderBottomRightRadius: true, + borderBottomStartRadius: true, + borderColor: colorAttributes, + borderEndColor: colorAttributes, + borderLeftColor: colorAttributes, + borderRadius: true, + borderRightColor: colorAttributes, + borderStartColor: colorAttributes, + borderStyle: true, + borderTopColor: colorAttributes, + borderTopEndRadius: true, + borderTopLeftRadius: true, + borderTopRightRadius: true, + borderTopStartRadius: true, + opacity: true, -ReactNativeStyleAttributes.transform = {process: processTransform}; -ReactNativeStyleAttributes.shadowOffset = {diff: sizesDiffer}; + /** + * Text + */ + color: colorAttributes, + fontFamily: true, + fontSize: true, + fontStyle: true, + fontVariant: true, + fontWeight: true, + includeFontPadding: true, + letterSpacing: true, + lineHeight: true, + textAlign: true, + textAlignVertical: true, + textDecorationColor: colorAttributes, + textDecorationLine: true, + textDecorationStyle: true, + textShadowColor: colorAttributes, + textShadowOffset: true, + textShadowRadius: true, + textTransform: true, + writingDirection: true, -const colorAttributes = {process: processColor}; -ReactNativeStyleAttributes.backgroundColor = colorAttributes; -ReactNativeStyleAttributes.borderBottomColor = colorAttributes; -ReactNativeStyleAttributes.borderColor = colorAttributes; -ReactNativeStyleAttributes.borderLeftColor = colorAttributes; -ReactNativeStyleAttributes.borderRightColor = colorAttributes; -ReactNativeStyleAttributes.borderTopColor = colorAttributes; -ReactNativeStyleAttributes.borderStartColor = colorAttributes; -ReactNativeStyleAttributes.borderEndColor = colorAttributes; -ReactNativeStyleAttributes.color = colorAttributes; -ReactNativeStyleAttributes.shadowColor = colorAttributes; -ReactNativeStyleAttributes.textDecorationColor = colorAttributes; -ReactNativeStyleAttributes.tintColor = colorAttributes; -ReactNativeStyleAttributes.textShadowColor = colorAttributes; -ReactNativeStyleAttributes.overlayColor = colorAttributes; + /** + * Image + */ + overlayColor: colorAttributes, + resizeMode: true, + tintColor: colorAttributes, +}; module.exports = ReactNativeStyleAttributes; diff --git a/Libraries/Components/View/ReactNativeViewAttributes.js b/Libraries/Components/View/ReactNativeViewAttributes.js index 7f9452fc9447ec..2e5ffda73f9105 100644 --- a/Libraries/Components/View/ReactNativeViewAttributes.js +++ b/Libraries/Components/View/ReactNativeViewAttributes.js @@ -9,8 +9,7 @@ */ 'use strict'; - -const ReactNativeStyleAttributes = require('./ReactNativeStyleAttributes'); +import ReactNativeStyleAttributes from './ReactNativeStyleAttributes'; const UIView = { pointerEvents: true, diff --git a/Libraries/Components/View/ReactNativeViewViewConfig.js b/Libraries/Components/View/ReactNativeViewViewConfig.js index 4e0c813f71357b..0a3a27ac0370df 100644 --- a/Libraries/Components/View/ReactNativeViewViewConfig.js +++ b/Libraries/Components/View/ReactNativeViewViewConfig.js @@ -8,17 +8,17 @@ * @format */ -'use strict'; +import type {ViewConfig} from '../../Renderer/shims/ReactNativeTypes'; import ReactNativeViewViewConfigAndroid from './ReactNativeViewViewConfigAndroid'; import ReactNativeViewViewConfigMacOS from './ReactNativeViewViewConfigMacOS'; // TODO(macOS GH#774) import {Platform} from 'react-native'; -const ReactNativeViewConfig = { +const ReactNativeViewConfig: ViewConfig = { uiViewClassName: 'RCTView', baseModuleName: null, Manager: 'ViewManager', - Commands: ({}: {...}), - Constants: ({}: {...}), + Commands: {}, + Constants: {}, bubblingEventTypes: { ...ReactNativeViewViewConfigAndroid.bubblingEventTypes, topBlur: { @@ -180,7 +180,7 @@ const ReactNativeViewConfig = { flexShrink: true, flexWrap: true, height: true, - hitSlop: {diff: (require('../../Utilities/differ/insetsDiffer'): any)}, + hitSlop: {diff: require('../../Utilities/differ/insetsDiffer')}, importantForAccessibility: true, justifyContent: true, left: true, @@ -332,9 +332,10 @@ const ReactNativeViewConfig = { textTransform: true, tintColor: {process: require('../../StyleSheet/processColor')}, top: true, - transform: ((Platform.OS === 'ios' || Platform.OS === 'macos' // TODO(macOS GH#774) - ? {diff: require('../../Utilities/differ/matricesDiffer')} - : {process: require('../../StyleSheet/processTransform')}): any), + transform: + Platform.OS === 'ios' || Platform.OS === 'macos' // TODO(macOS GH#774) + ? {diff: require('../../Utilities/differ/matricesDiffer')} + : {process: require('../../StyleSheet/processTransform')}, transformMatrix: true, translateX: true, translateY: true, @@ -344,9 +345,10 @@ const ReactNativeViewConfig = { }, testID: true, top: true, - transform: ((Platform.OS === 'ios' || Platform.OS === 'macos' // TODO(macOS GH#774) - ? {diff: require('../../Utilities/differ/matricesDiffer')} - : {process: require('../../StyleSheet/processTransform')}): any), + transform: + Platform.OS === 'ios' || Platform.OS === 'macos' // TODO(macOS GH#774) + ? {diff: require('../../Utilities/differ/matricesDiffer')} + : {process: require('../../StyleSheet/processTransform')}, translateX: true, translateY: true, validKeysDown: true, diff --git a/Libraries/Components/View/View.js b/Libraries/Components/View/View.js index c221e4991bc753..a1b7a7abb277ed 100644 --- a/Libraries/Components/View/View.js +++ b/Libraries/Components/View/View.js @@ -8,14 +8,12 @@ * @flow strict-local */ -'use strict'; - import type {ViewProps} from './ViewPropTypes'; -const React = require('react'); import ViewNativeComponent from './ViewNativeComponent'; -const TextAncestor = require('../../Text/TextAncestor'); +import TextAncestor from '../../Text/TextAncestor'; import warnOnce from '../../Utilities/warnOnce'; // [macOS #656] +import * as React from 'react'; export type Props = ViewProps; diff --git a/Libraries/Components/View/ViewAccessibility.js b/Libraries/Components/View/ViewAccessibility.js index 485b208b411fb4..9c056b64c9c753 100644 --- a/Libraries/Components/View/ViewAccessibility.js +++ b/Libraries/Components/View/ViewAccessibility.js @@ -21,6 +21,7 @@ export type AccessibilityNodeInfoProp = { export type AccessibilityRole = | 'none' | 'button' + | 'togglebutton' | 'link' | 'search' | 'image' @@ -43,11 +44,13 @@ export type AccessibilityRole = | 'spinbutton' | 'switch' | 'tab' + | 'tabbar' | 'tablist' | 'timer' + | 'list' | 'toolbar' | 'popupbutton' - | 'menubutton'; + | 'menubutton'; // TODO(macOS GH#774) // the info associated with an accessibility action export type AccessibilityActionInfo = $ReadOnly<{ diff --git a/Libraries/Components/View/ViewNativeComponent.js b/Libraries/Components/View/ViewNativeComponent.js index 902414c4eb45e3..4ca5ec9cf3c479 100644 --- a/Libraries/Components/View/ViewNativeComponent.js +++ b/Libraries/Components/View/ViewNativeComponent.js @@ -4,72 +4,25 @@ * 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 - * @flow */ -'use strict'; - -const Platform = require('../../Utilities/Platform'); -const ReactNativeViewViewConfigAndroid = require('./ReactNativeViewViewConfigAndroid'); - -const registerGeneratedViewConfig = require('../../Utilities/registerGeneratedViewConfig'); -const requireNativeComponent = require('../../ReactNative/requireNativeComponent'); - -import * as React from 'react'; - +import * as NativeComponentRegistry from '../../NativeComponent/NativeComponentRegistry'; +import {type HostComponent} from '../../Renderer/shims/ReactNativeTypes'; +import Platform from '../../Utilities/Platform'; import codegenNativeCommands from '../../Utilities/codegenNativeCommands'; -import type {ViewProps} from './ViewPropTypes'; -import type {HostComponent} from '../../Renderer/shims/ReactNativeTypes'; - -export type ViewNativeComponentType = HostComponent; - -let NativeViewComponent; -let viewConfig: - | {...} - | {| - bubblingEventTypes?: $ReadOnly<{ - [eventName: string]: $ReadOnly<{| - phasedRegistrationNames: $ReadOnly<{| - bubbled: string, - captured: string, - |}>, - |}>, - ..., - }>, - directEventTypes?: $ReadOnly<{ - [eventName: string]: $ReadOnly<{|registrationName: string|}>, - ..., - }>, - uiViewClassName: string, - validAttributes?: { - [propName: string]: - | true - | $ReadOnly<{| - diff?: (arg1: any, arg2: any) => boolean, - process?: (arg1: any) => any, - |}>, - ..., - }, - |}; - -if (__DEV__ || global.RN$Bridgeless) { - // On Android, View extends the base component with additional view-only props - // On iOS, the base component is View - if (Platform.OS === 'android') { - viewConfig = ReactNativeViewViewConfigAndroid; - registerGeneratedViewConfig('RCTView', ReactNativeViewViewConfigAndroid); - } else { - viewConfig = {}; - registerGeneratedViewConfig('RCTView', {uiViewClassName: 'RCTView'}); - } - - NativeViewComponent = 'RCTView'; -} else { - NativeViewComponent = requireNativeComponent('RCTView'); -} +import ReactNativeViewViewConfigAndroid from './ReactNativeViewViewConfigAndroid'; +import {type ViewProps as Props} from './ViewPropTypes'; +import * as React from 'react'; -export const __INTERNAL_VIEW_CONFIG = viewConfig; +const ViewNativeComponent: HostComponent = NativeComponentRegistry.get( + 'RCTView', + () => + Platform.OS === 'android' + ? ReactNativeViewViewConfigAndroid + : {uiViewClassName: 'RCTView'}, +); interface NativeCommands { +hotspotUpdate: ( @@ -87,4 +40,6 @@ export const Commands: NativeCommands = codegenNativeCommands({ supportedCommands: ['hotspotUpdate', 'setPressed'], }); -export default ((NativeViewComponent: any): ViewNativeComponentType); +export default ViewNativeComponent; + +export type ViewNativeComponentType = HostComponent; diff --git a/Libraries/Components/__tests__/Button-test.js b/Libraries/Components/__tests__/Button-test.js new file mode 100644 index 00000000000000..f81f9bc63d229e --- /dev/null +++ b/Libraries/Components/__tests__/Button-test.js @@ -0,0 +1,49 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import * as React from 'react'; +import ReactTestRenderer from 'react-test-renderer'; +import Button from '../Button'; + +describe('