diff --git a/.circleci/config.yml b/.circleci/config.yml index a642711e33d1af..ff5476dd49dff4 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -71,7 +71,7 @@ jobs: name: "[Main or Stable] Create input for config to test everything" command: | mkdir -p /tmp/circleci/ - echo '{ "run_all": true }' > /tmp/circleci/pipeline_config.json + node ./scripts/circleci/pipeline_selection.js filter-jobs - when: condition: not: diff --git a/.circleci/configurations/executors.yml b/.circleci/configurations/executors.yml index 3884b21abfdfc1..360049670c2503 100644 --- a/.circleci/configurations/executors.yml +++ b/.circleci/configurations/executors.yml @@ -36,3 +36,17 @@ executors: resource_class: macos.x86.medium.gen2 environment: - RCT_BUILD_HERMES_FROM_SOURCE: true + reactnativeios-lts: + <<: *defaults + macos: + xcode: '14.3.1' + resource_class: macos.x86.medium.gen2 + environment: + - RCT_BUILD_HERMES_FROM_SOURCE: true + reactnative-visionos: + <<: *defaults + resource_class: macos.m1.medium.gen1 + macos: + xcode: '15.2' + environment: + - RCT_BUILD_HERMES_FROM_SOURCE: true diff --git a/.circleci/configurations/jobs.yml b/.circleci/configurations/jobs.yml index 7f14131a504818..4eddadb0b13085 100644 --- a/.circleci/configurations/jobs.yml +++ b/.circleci/configurations/jobs.yml @@ -96,8 +96,8 @@ jobs: - run: name: "Run Tests: JavaScript Tests" command: node ./scripts/run-ci-javascript-tests.js --maxWorkers 2 - - run_e2e: - platform: js + # - run_e2e: + # platform: js # Optionally, run disabled tests - when: @@ -360,7 +360,7 @@ jobs: command: | REPO_ROOT=$(pwd) node ./scripts/releases/update-template-package.js "{\"react-native\":\"file:$REPO_ROOT/build/$(cat build/react-native-package-version)\"}" - node ./scripts/e2e/init-template-e2e.js --projectName $PROJECT_NAME --templatePath "$REPO_ROOT/packages/react-native" --directory "/tmp/$PROJECT_NAME" --verbose + node ./scripts/template/initialize.js --reactNativeRootPath $REPO_ROOT --templateName $PROJECT_NAME --templateConfigPath "$REPO_ROOT/packages/react-native" --directory "/tmp/$PROJECT_NAME" - with_gradle_cache: steps: - run: @@ -458,7 +458,7 @@ jobs: PACKAGE=$(cat build/react-native-package-version) PATH_TO_PACKAGE="$REPO_ROOT/build/$PACKAGE" node ./scripts/releases/update-template-package.js "{\"react-native\":\"file:$PATH_TO_PACKAGE\"}" - node ./scripts/e2e/init-template-e2e.js --projectName $PROJECT_NAME --templatePath "$REPO_ROOT/packages/react-native" --directory "/tmp/$PROJECT_NAME" --verbose + node ./scripts/template/initialize.js --reactNativeRootPath $REPO_ROOT --templateName $PROJECT_NAME --templateConfigPath "$REPO_ROOT/packages/react-native" --directory "/tmp/$PROJECT_NAME" - with_xcodebuild_cache: podfile_lock_path: << parameters.podfile_lock_path >> pods_build_folder: << parameters.pods_build_folder >> @@ -590,6 +590,74 @@ jobs: steps: - run_ios_tests + # ------------------------- + # JOBS: Test visionOS RNTester + # ------------------------- + test_visionos_rntester: + + parameters: + jsengine: + default: "JSC" + description: Which JavaScript engine to use. Must be one of "Hermes", "JSC". + type: enum + enum: ["Hermes", "JSC"] + architecture: + default: "OldArch" + description: Which React Native architecture to use. Must be one of "OldArch", "NewArch". + type: enum + enum: ["NewArch", "OldArch"] + use_frameworks: + default: "StaticLibraries" + description: The dependency building and linking strategy to use. Must be one of "StaticLibraries", "DynamicFrameworks" + type: enum + enum: ["StaticLibraries", "DynamicFrameworks"] + ruby_version: + default: "2.6.10" + description: The version of ruby that must be used + type: string + run_unit_tests: + description: whether unit tests should run or not. + default: false + type: boolean + executor: + description: The executor to use + default: reactnative-visionos + type: string + executor: << parameters.executor >> + steps: + - checkout_code_with_cache + - run_yarn + - setup_ruby: + ruby_version: << parameters.ruby_version >> + - with_xcodebuild_cache: + steps: + - run: + name: "Install pods" + command: | + if [[ << parameters.architecture >> == "NewArch" ]]; then + export RCT_NEW_ARCH_ENABLED=1 + fi + + if [[ << parameters.jsengine >> == "JSC" ]]; then + export USE_HERMES=0 + fi + + if [[ << parameters.use_frameworks >> == "DynamicFrameworks" ]]; then + export USE_FRAMEWORKS=dynamic + fi + + cd packages/rn-tester + + bundle install + bundle exec pod install + - run: + name: "Build rn-tester" + command: | + xcodebuild build \ + -workspace packages/rn-tester/RNTesterPods.xcworkspace \ + -scheme RNTester-visionOS \ + -sdk xrsimulator + # ------------------------- # JOBS: Build Hermes # ------------------------- @@ -1286,4 +1354,4 @@ jobs: command: echo "//registry.npmjs.org/:_authToken=${CIRCLE_NPM_TOKEN}" > ~/.npmrc - run: name: Find and publish all bumped packages - command: node ./scripts/releases-ci/publish-updated-packages.js + command: node ./scripts/monorepo/find-and-publish-all-bumped-packages.js diff --git a/.circleci/configurations/test_workflows/testAll.yml b/.circleci/configurations/test_workflows/testAll.yml index 03ba79b8e388c7..4f41ddad44b43c 100644 --- a/.circleci/configurations/test_workflows/testAll.yml +++ b/.circleci/configurations/test_workflows/testAll.yml @@ -11,8 +11,8 @@ tag: test dry_run: true - prepare_hermes_workspace - - build_android: - release_type: "dry-run" + # - build_android: + # release_type: "dry-run" - build_hermesc_linux: requires: - prepare_hermes_workspace @@ -39,21 +39,21 @@ # Build a release package on every untagged commit, but do not publish to npm. release_type: "dry-run" requires: - - build_android + # - build_android - build_hermesc_linux - build_hermes_macos - build_hermesc_windows - - test_android: - requires: - - build_android - - test_android_template: - requires: - - build_npm_package - matrix: - parameters: - architecture: ["NewArch", "OldArch"] - jsengine: ["Hermes", "JSC"] - flavor: ["Debug", "Release"] + # - test_android: + # requires: + # - build_android + # - test_android_template: + # requires: + # - build_npm_package + # matrix: + # parameters: + # architecture: ["NewArch", "OldArch"] + # jsengine: ["Hermes", "JSC"] + # flavor: ["Debug", "Release"] - test_ios_template: requires: - build_npm_package diff --git a/.circleci/configurations/test_workflows/testAndroid.yml b/.circleci/configurations/test_workflows/testAndroid.yml index 83a1d0e36c99c9..3613a212693cf0 100644 --- a/.circleci/configurations/test_workflows/testAndroid.yml +++ b/.circleci/configurations/test_workflows/testAndroid.yml @@ -1,6 +1,7 @@ tests_android: when: and: + - equal: [ true, false ] # Disable for visionOS - equal: [ false, << pipeline.parameters.run_release_workflow >> ] - equal: [ false, << pipeline.parameters.run_nightly_workflow >> ] jobs: diff --git a/.circleci/configurations/test_workflows/testE2E.yml b/.circleci/configurations/test_workflows/testE2E.yml index 998abf796ffe8c..6f0704a514cf0d 100644 --- a/.circleci/configurations/test_workflows/testE2E.yml +++ b/.circleci/configurations/test_workflows/testE2E.yml @@ -1,6 +1,7 @@ tests_e2e: when: and: + - equal: [ true, false ] # Disable for visionOS - equal: [ false, << pipeline.parameters.run_release_workflow >> ] - equal: [ false, << pipeline.parameters.run_nightly_workflow >> ] jobs: diff --git a/.circleci/configurations/test_workflows/testIOS.yml b/.circleci/configurations/test_workflows/testIOS.yml index 3e8e24239e1952..d380ea6732e283 100644 --- a/.circleci/configurations/test_workflows/testIOS.yml +++ b/.circleci/configurations/test_workflows/testIOS.yml @@ -1,6 +1,7 @@ test_ios: when: and: + - equal: [ true, false ] # Disable for visionOS - equal: [ false, << pipeline.parameters.run_release_workflow >> ] - equal: [ false, << pipeline.parameters.run_nightly_workflow >> ] jobs: diff --git a/.circleci/configurations/test_workflows/testVisionOS.yml b/.circleci/configurations/test_workflows/testVisionOS.yml new file mode 100644 index 00000000000000..37f3fd3f6a9c9d --- /dev/null +++ b/.circleci/configurations/test_workflows/testVisionOS.yml @@ -0,0 +1,6 @@ + test_visionos: + jobs: + - test_visionos_rntester: + matrix: + parameters: + architecture: ["OldArch", "NewArch"] diff --git a/.circleci/configurations/top_level.yml b/.circleci/configurations/top_level.yml index 2167897d295fe1..09f793ad2ec967 100644 --- a/.circleci/configurations/top_level.yml +++ b/.circleci/configurations/top_level.yml @@ -74,25 +74,25 @@ references: gems_cache_key: &gems_cache_key v1-gems-{{ checksum "Gemfile.lock" }} gradle_cache_key: &gradle_cache_key v3-gradle-{{ .Environment.CIRCLE_JOB }}-{{ checksum "gradle/wrapper/gradle-wrapper.properties" }}-{{ checksum "packages/react-native/ReactAndroid/gradle.properties" }} yarn_cache_key: &yarn_cache_key v6-yarn-cache-{{ .Environment.CIRCLE_JOB }} - rbenv_cache_key: &rbenv_cache_key v1-rbenv-{{ checksum "/tmp/required_ruby" }} - hermes_workspace_cache_key: &hermes_workspace_cache_key v5-hermes-{{ .Environment.CIRCLE_JOB }}-{{ checksum "/tmp/hermes/hermesversion" }} - hermes_workspace_debug_cache_key: &hermes_workspace_debug_cache_key v2-hermes-{{ .Environment.CIRCLE_JOB }}-debug-{{ checksum "/tmp/hermes/hermesversion" }}-{{ checksum "/tmp/react-native-version" }}-{{ checksum "packages/react-native/sdks/hermes-engine/utils/build-apple-framework.sh" }} - hermes_workspace_release_cache_key: &hermes_workspace_release_cache_key v2-hermes-{{ .Environment.CIRCLE_JOB }}-release-{{ checksum "/tmp/hermes/hermesversion" }}-{{ checksum "/tmp/react-native-version" }}-{{ checksum "packages/react-native/sdks/hermes-engine/utils/build-apple-framework.sh" }} - hermes_linux_cache_key: &hermes_linux_cache_key v1-hermes-{{ .Environment.CIRCLE_JOB }}-linux-{{ checksum "/tmp/hermes/hermesversion" }}-{{ checksum "/tmp/react-native-version" }} - hermes_windows_cache_key: &hermes_windows_cache_key v2-hermes-{{ .Environment.CIRCLE_JOB }}-windows-{{ checksum "/Users/circleci/project/tmp/hermes/hermesversion" }}-{{ checksum "/tmp/react-native-version" }} + rbenv_cache_key: &rbenv_cache_key v2-rbenv-{{ checksum "/tmp/required_ruby" }} + hermes_workspace_cache_key: &hermes_workspace_cache_key v7-visionos-hermes-{{ .Environment.CIRCLE_JOB }}-{{ checksum "/tmp/hermes/hermesversion" }} + hermes_workspace_debug_cache_key: &hermes_workspace_debug_cache_key v2-visionos-hermes-{{ .Environment.CIRCLE_JOB }}-debug-{{ checksum "/tmp/hermes/hermesversion" }}-{{ checksum "/tmp/react-native-version" }}-{{ checksum "packages/react-native/sdks/hermes-engine/utils/build-apple-framework.sh" }} + hermes_workspace_release_cache_key: &hermes_workspace_release_cache_key v2-visionos-hermes-{{ .Environment.CIRCLE_JOB }}-release-{{ checksum "/tmp/hermes/hermesversion" }}-{{ checksum "/tmp/react-native-version" }}-{{ checksum "packages/react-native/sdks/hermes-engine/utils/build-apple-framework.sh" }} + hermes_linux_cache_key: &hermes_linux_cache_key v1-visionos-hermes-{{ .Environment.CIRCLE_JOB }}-linux-{{ checksum "/tmp/hermes/hermesversion" }}-{{ checksum "/tmp/react-native-version" }} + hermes_windows_cache_key: &hermes_windows_cache_key v2-visionos-hermes-{{ .Environment.CIRCLE_JOB }}-windows-{{ checksum "/Users/circleci/project/tmp/hermes/hermesversion" }}-{{ checksum "/tmp/react-native-version" }} # Hermes iOS - hermesc_apple_cache_key: &hermesc_apple_cache_key v3-hermesc-apple-{{ checksum "/tmp/hermes/hermesversion" }}-{{ checksum "/tmp/react-native-version" }} - hermes_apple_slices_cache_key: &hermes_apple_slices_cache_key v7-hermes-apple-{{ checksum "/tmp/hermes/hermesversion" }}-{{ checksum "/tmp/react-native-version" }}-{{ checksum "packages/react-native/sdks/hermes-engine/utils/build-apple-framework.sh" }} - hermes_tarball_debug_cache_key: &hermes_tarball_debug_cache_key v5-hermes-tarball-debug-{{ checksum "/tmp/hermes/hermesversion" }}-{{ checksum "/tmp/react-native-version" }}-{{ checksum "packages/react-native/sdks/hermes-engine/utils/build-apple-framework.sh" }} - hermes_tarball_release_cache_key: &hermes_tarball_release_cache_key v4-hermes-tarball-release-{{ checksum "/tmp/hermes/hermesversion" }}-{{ checksum "/tmp/react-native-version" }}-{{ checksum "packages/react-native/sdks/hermes-engine/utils/build-apple-framework.sh" }} - hermes_macosx_bin_release_cache_key: &hermes_macosx_bin_release_cache_key v4-hermes-release-macosx-{{ checksum "/tmp/hermes/hermesversion" }}-{{ checksum "/tmp/react-native-version" }} - hermes_macosx_bin_debug_cache_key: &hermes_macosx_bin_debug_cache_key v2-hermes-debug-macosx-{{ checksum "/tmp/hermes/hermesversion" }}-{{ checksum "/tmp/react-native-version" }} - hermes_dsym_debug_cache_key: &hermes_dsym_debug_cache_key v2-hermes-debug-dsym-{{ checksum "/tmp/hermes/hermesversion" }}-{{ checksum "/tmp/react-native-version" }} - hermes_dsym_release_cache_key: &hermes_dsym_release_cache_key v2-hermes-release-dsym-{{ checksum "/tmp/hermes/hermesversion" }}-{{ checksum "/tmp/react-native-version" }} + hermesc_apple_cache_key: &hermesc_apple_cache_key v4-visionos-hermesc-apple-{{ checksum "/tmp/hermes/hermesversion" }}-{{ checksum "/tmp/react-native-version" }} + hermes_apple_slices_cache_key: &hermes_apple_slices_cache_key v4-visionos-hermes-apple-{{ checksum "/tmp/hermes/hermesversion" }}-{{ checksum "/tmp/react-native-version" }}-{{ checksum "packages/react-native/sdks/hermes-engine/utils/build-apple-framework.sh" }} + hermes_tarball_debug_cache_key: &hermes_tarball_debug_cache_key v7-visionos-hermes-tarball-debug-{{ checksum "/tmp/hermes/hermesversion" }}-{{ checksum "/tmp/react-native-version" }}-{{ checksum "packages/react-native/sdks/hermes-engine/utils/build-apple-framework.sh" }} + hermes_tarball_release_cache_key: &hermes_tarball_release_cache_key v4-visionos-hermes-tarball-release-{{ checksum "/tmp/hermes/hermesversion" }}-{{ checksum "/tmp/react-native-version" }}-{{ checksum "packages/react-native/sdks/hermes-engine/utils/build-apple-framework.sh" }} + hermes_macosx_bin_release_cache_key: &hermes_macosx_bin_release_cache_key v2-visionos-hermes-release-macosx-{{ checksum "/tmp/hermes/hermesversion" }}-{{ checksum "/tmp/react-native-version" }} + hermes_macosx_bin_debug_cache_key: &hermes_macosx_bin_debug_cache_key v2-visionos-hermes-debug-macosx-{{ checksum "/tmp/hermes/hermesversion" }}-{{ checksum "/tmp/react-native-version" }} + hermes_dsym_debug_cache_key: &hermes_dsym_debug_cache_key v2-visionos-hermes-debug-dsym-{{ checksum "/tmp/hermes/hermesversion" }}-{{ checksum "/tmp/react-native-version" }} + hermes_dsym_release_cache_key: &hermes_dsym_release_cache_key v2-visionos-hermes-release-dsym-{{ checksum "/tmp/hermes/hermesversion" }}-{{ checksum "/tmp/react-native-version" }} # Cocoapods - RNTester - pods_cache_key: &pods_cache_key v11-pods-{{ .Environment.CIRCLE_JOB }}-{{ checksum "packages/rn-tester/Podfile.lock.bak" }}-{{ checksum "packages/rn-tester/Podfile" }} - cocoapods_cache_key: &cocoapods_cache_key v11-cocoapods-{{ .Environment.CIRCLE_JOB }}-{{ checksum "packages/rn-tester/Podfile.lock" }}-{{ checksum "packages/rn-tester/Podfile" }}-{{ checksum "/tmp/hermes/hermesversion" }} - rntester_podfile_lock_cache_key: &rntester_podfile_lock_cache_key v10-podfilelock-{{ .Environment.CIRCLE_JOB }}-{{ checksum "packages/rn-tester/Podfile" }}-{{ checksum "/tmp/week_year" }}-{{ checksum "/tmp/hermes/hermesversion" }} + pods_cache_key: &pods_cache_key v11-visionos-pods-{{ .Environment.CIRCLE_JOB }}-{{ checksum "packages/rn-tester/Podfile.lock.bak" }}-{{ checksum "packages/rn-tester/Podfile" }} + cocoapods_cache_key: &cocoapods_cache_key v11-visionos-cocoapods-{{ .Environment.CIRCLE_JOB }}-{{ checksum "packages/rn-tester/Podfile.lock" }}-{{ checksum "packages/rn-tester/Podfile" }} + rntester_podfile_lock_cache_key: &rntester_podfile_lock_cache_key v9-visionos-podfilelock-{{ .Environment.CIRCLE_JOB }}-{{ checksum "packages/rn-tester/Podfile" }}-{{ checksum "/tmp/week_year" }} # Cocoapods - Template template_cocoapods_cache_key: &template_cocoapods_cache_key v6-cocoapods-{{ .Environment.CIRCLE_JOB }}-{{ checksum "/tmp/iOSTemplateProject/ios/Podfile.lock" }}-{{ checksum "/tmp/iOSTemplateProject/ios/Podfile" }}-{{ checksum "/tmp/hermes/hermesversion" }}-{{ checksum "packages/rn-tester/Podfile.lock" }} diff --git a/.circleci/configurations/workflows.yml b/.circleci/configurations/workflows.yml index e74f8ce39cd132..7bd1cf32c7bc34 100644 --- a/.circleci/configurations/workflows.yml +++ b/.circleci/configurations/workflows.yml @@ -128,6 +128,7 @@ workflows: publish_bumped_packages: when: and: + - equal: [ 'https://github.com/facebook/react-native', << pipeline.project.git_url >> ] - equal: [ false, << pipeline.parameters.run_release_workflow >> ] - equal: [ false, << pipeline.parameters.run_nightly_workflow >> ] jobs: diff --git a/.github/workflows/ios-tests.yml b/.github/workflows/ios-tests.yml index d2aae0c7791801..84daa3ebc06a18 100644 --- a/.github/workflows/ios-tests.yml +++ b/.github/workflows/ios-tests.yml @@ -1,12 +1,18 @@ name: ios-tests + +# on: +# push: +# branches: +# - main +# pull_request: +# branches: +# - "*" + +# For visionOS we run iOS tests every night on: - push: - branches: - - main - pull_request: - branches: - - "*" + schedule: + - cron: '0 0 * * *' jobs: test_ios_rntester-Hermes: diff --git a/.gitignore b/.gitignore index b331cd3cd3ea91..8eb742809e30c3 100644 --- a/.gitignore +++ b/.gitignore @@ -163,3 +163,6 @@ vendor/ # CircleCI .circleci/generated_config.yml +.circleci/storage + +.yarn diff --git a/README.md b/README.md index 4d8883c511c666..fe5b8e5e1b2103 100644 --- a/README.md +++ b/README.md @@ -1,147 +1,52 @@

- - React Native + + React Native visionOS

Learn once, write anywhere:
- Build mobile apps with React. + Build spatial apps with React.

-

- - React Native is released under the MIT license. - - - Current CircleCI build status. - - - Current npm package version. - - - PRs welcome! - - - Follow @reactnative - -

- -

- Getting Started - · - Learn the Basics - · - Showcase - · - Contribute - · - Community - · - Support -

- -React Native brings [**React**'s][r] declarative UI framework to iOS and Android. With React Native, you use native UI controls and have full access to the native platform. - -- **Declarative.** React makes it painless to create interactive UIs. Declarative views make your code more predictable and easier to debug. -- **Component-Based.** Build encapsulated components that manage their state, then compose them to make complex UIs. -- **Developer Velocity.** See local changes in seconds. Changes to JavaScript code can be live reloaded without rebuilding the native app. -- **Portability.** Reuse code across iOS, Android, and [other platforms][p]. - -React Native is developed and supported by many companies and individual core contributors. Find out more in our [ecosystem overview][e]. - -[r]: https://react.dev/ -[p]: https://reactnative.dev/docs/out-of-tree-platforms -[e]: https://github.com/facebook/react-native/blob/HEAD/ECOSYSTEM.md - -## Contents - -- [Requirements](#-requirements) -- [Building your first React Native app](#-building-your-first-react-native-app) -- [Documentation](#-documentation) -- [Upgrading](#-upgrading) -- [How to Contribute](#-how-to-contribute) -- [Code of Conduct](#code-of-conduct) -- [License](#-license) - - -## 📋 Requirements - -React Native apps may target iOS 13.4 and Android 6.0 (API 23) or newer. You may use Windows, macOS, or Linux as your development operating system, though building and running iOS apps is limited to macOS. Tools like [Expo](https://expo.dev) can be used to work around this. +React Native visionOS allows you to write visionOS with full support for platform SDK. This is a full fork of the main repository with changes needed to support visionOS. -## 🎉 Building your first React Native app +![Screenshot](https://github.com/callstack/react-native-visionos/assets/52801365/0fcd5e5f-628c-49ef-84ab-d1d4675a011a) -Follow the [Getting Started guide](https://reactnative.dev/docs/getting-started). The recommended way to install React Native depends on your project. Here you can find short guides for the most common scenarios: +## 🎉 Building your first spatial React Native app +Follow the [Getting Started](https://callstack.github.io/react-native-visionos-docs/category/getting-started) guide. If you wish to get started quickly, you can utilize this command: -- [Trying out React Native][hello-world] -- [Creating a New Application][new-app] -- [Adding React Native to an Existing Application][existing] +```sh +npx @callstack/react-native-visionos@latest init YourApp +``` -[hello-world]: https://snack.expo.dev/@samples/hello-world -[new-app]: https://reactnative.dev/docs/getting-started -[existing]: https://reactnative.dev/docs/integration-with-existing-apps ## 📖 Documentation -The full documentation for React Native can be found on our [website][docs]. - -The React Native documentation discusses components, APIs, and topics that are specific to React Native. For further documentation on the React API that is shared between React Native and React DOM, refer to the [React documentation][r-docs]. - -The source for the React Native documentation and website is hosted on a separate repo, [**@facebook/react-native-website**][repo-website]. - -[docs]: https://reactnative.dev/docs/getting-started -[r-docs]: https://react.dev/learn -[repo-website]: https://github.com/facebook/react-native-website - -## 🚀 Upgrading - -Upgrading to new versions of React Native may give you access to more APIs, views, developer tools, and other goodies. See the [Upgrading Guide][u] for instructions. - -React Native releases are discussed [in this discussion repo](https://github.com/reactwg/react-native-releases/discussions). - -[u]: https://reactnative.dev/docs/upgrading -[repo-releases]: https://github.com/react-native-community/react-native-releases - -## 👏 How to Contribute - -The main purpose of this repository is to continue evolving React Native core. We want to make contributing to this project as easy and transparent as possible, and we are grateful to the community for contributing bug fixes and improvements. Read below to learn how you can take part in improving React Native. - -### [Code of Conduct][code] - -Facebook has adopted a Code of Conduct that we expect project participants to adhere to. -Please read the [full text][code] so that you can understand what actions will and will not be tolerated. - -[code]: https://code.fb.com/codeofconduct/ - -### [Contributing Guide][contribute] - -Read our [**Contributing Guide**][contribute] to learn about our development process, how to propose bugfixes and improvements, and how to build and test your changes to React Native. - -[contribute]: https://reactnative.dev/docs/contributing - -### [Open Source Roadmap][roadmap] - -You can learn more about our vision for React Native in the [**Roadmap**][roadmap]. +The full documentation for React Native visionOS can be found on our [website](https://callstack.github.io/react-native-visionos-docs). -[roadmap]: https://github.com/facebook/react-native/wiki/Roadmap +The source for the React Native visionOS documentation and website is hosted on a separate repo, @callstack/react-native-visionos-docs. -### Good First Issues +## Contributing -We have a list of [good first issues][gfi] that contain bugs which have a relatively limited scope. This is a great place to get started, gain experience, and get familiar with our contribution process. +Prerequisites: +- Download the latest Xcode (at least 15.2) +- Install the latest version of CMake (at least v3.28.0) -[gfi]: https://github.com/facebook/react-native/labels/good%20first%20issue +Check out `rn-tester` [README.md](./packages/rn-tester/README.md) to build React Native from the source. -### Discussions +Remember to use `RNTester-visionOS` target -Larger discussions and proposals are discussed in [**@react-native-community/discussions-and-proposals**][repo-meta]. +If `RNTester-visionOS` scheme is not showing up, click "New Scheme", which should be pre-populated with `RNTester-visionOS`. Build the app using Xcode. -[repo-meta]: https://github.com/react-native-community/discussions-and-proposals +## Release process -## 📄 License +We use a script called `oot-release.js` which automatically releases `visionos` packages and aligns versions of dependencies with React Native core. -React Native is MIT licensed, as found in the [LICENSE][l] file. +Usage: -React Native documentation is Creative Commons licensed, as found in the [LICENSE-docs][ld] file. +```sh +node ./scripts/oot-release.js --new-version "" --react-native-version "" --one-time-password "" +``` -[l]: https://github.com/facebook/react-native/blob/main/LICENSE -[ld]: https://github.com/facebook/react-native/blob/main/LICENSE-docs +To test releases and template we use [Verdaccio](https://verdaccio.org/). diff --git a/jest.config.js b/jest.config.js index 933d17ec51500f..fcec1dc447fa43 100644 --- a/jest.config.js +++ b/jest.config.js @@ -41,6 +41,15 @@ module.exports = { defaultPlatform: 'ios', platforms: ['ios', 'android'], }, + moduleNameMapper: { + // These mappers allow out-of-tree platforms tests to seamlessly resolve RN imports + '^react-native/(.*)': '/packages/react-native/$1', + '^react-native$': '/packages/react-native/index.js', + // This module is internal to Meta and used by their custom React renderer. + // In tests, we can just use a mock. + '^ReactNativeInternalFeatureFlags$': + '/packages/react-native/jest/ReactNativeInternalFeatureFlagsMock.js', + }, moduleFileExtensions: ['fb.js'].concat(defaults.moduleFileExtensions), modulePathIgnorePatterns: ['scripts/.*/__fixtures__/'], unmockedModulePathPatterns: [ diff --git a/packages/out-of-tree-platforms/.gitignore b/packages/out-of-tree-platforms/.gitignore new file mode 100644 index 00000000000000..ac0720f94136cd --- /dev/null +++ b/packages/out-of-tree-platforms/.gitignore @@ -0,0 +1,2 @@ +# Build output +/dist \ No newline at end of file diff --git a/packages/out-of-tree-platforms/README.md b/packages/out-of-tree-platforms/README.md new file mode 100644 index 00000000000000..972663edaca00e --- /dev/null +++ b/packages/out-of-tree-platforms/README.md @@ -0,0 +1,19 @@ +# @callstack/out-of-tree-platforms + +[![Version][version-badge]][package] + +Utilities for Out of Tree (OOT) platforms. + +## `getPlatformResolver` + +```js +getPlatformResolver(options: ResolverConfig): CustomResolver +``` + +### options + +```js +type ResolverConfig = { + platformImplementations: {[platform: string]: string}, +}; +``` diff --git a/packages/out-of-tree-platforms/index.js.flow b/packages/out-of-tree-platforms/index.js.flow new file mode 100644 index 00000000000000..8420b1093fdb2f --- /dev/null +++ b/packages/out-of-tree-platforms/index.js.flow @@ -0,0 +1 @@ +export * from './src'; diff --git a/packages/out-of-tree-platforms/package.json b/packages/out-of-tree-platforms/package.json new file mode 100644 index 00000000000000..ee09627611fd41 --- /dev/null +++ b/packages/out-of-tree-platforms/package.json @@ -0,0 +1,27 @@ +{ + "name": "@callstack/out-of-tree-platforms", + "version": "0.75.0-main", + "description": "Utils for React Native out of tree platforms.", + "keywords": ["out-of-tree", "react-native"], + "homepage": "https://github.com/callstack/react-native-visionos/tree/HEAD/packages/out-of-tree-platforms#readme", + "bugs": "https://github.com/callstack/react-native-visionos/issues", + "repository": { + "type": "git", + "url": "https://github.com/callstack/react-native-visionos.git", + "directory": "packages/out-of-tree-platforms" + }, + "license": "MIT", + "exports": { + ".": "./src/index.js", + "./package.json": "./package.json" + }, + "files": [ + "dist" + ], + "devDependencies": { + "metro-resolver": "^0.80.0" + }, + "engines": { + "node": ">=18" + } +} diff --git a/packages/out-of-tree-platforms/src/getPlatformResolver.js b/packages/out-of-tree-platforms/src/getPlatformResolver.js new file mode 100644 index 00000000000000..70e3acde5c1d78 --- /dev/null +++ b/packages/out-of-tree-platforms/src/getPlatformResolver.js @@ -0,0 +1,43 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + * @format + */ + +import type {CustomResolver} from 'metro-resolver'; + +type ResolverConfig = { + platformNameMap: {[platform: string]: string}, +}; + +/** + * Creates a custom Metro resolver that maps platform extensions to package names. + * To be used in app's `metro.config.js` as `resolver.resolveRequest`. + */ +export const getPlatformResolver = (config: ResolverConfig): CustomResolver => { + return (context, moduleName, platform) => { + // `customResolverOptions` is populated through `?resolver.platformExtension` query params + // in the jsBundleURLForBundleRoot method of the react-native/React/Base/RCTBundleURLProvider.mm + const platformExtension = context.customResolverOptions?.platformExtension; + let modifiedModuleName = moduleName; + if ( + typeof platformExtension === 'string' && + config.platformNameMap?.[platformExtension] + ) { + const packageName = config.platformNameMap[platformExtension]; + if (moduleName === 'react-native') { + modifiedModuleName = packageName; + } else if (moduleName.startsWith('react-native/')) { + modifiedModuleName = `${packageName}/${modifiedModuleName.slice( + 'react-native/'.length, + )}`; + } + } + + return context.resolveRequest(context, modifiedModuleName, platform); + }; +}; diff --git a/packages/out-of-tree-platforms/src/index.js b/packages/out-of-tree-platforms/src/index.js new file mode 100644 index 00000000000000..2244b1ce77d29e --- /dev/null +++ b/packages/out-of-tree-platforms/src/index.js @@ -0,0 +1,5 @@ +if (!process.env.BUILD_EXCLUDE_BABEL_REGISTER) { + require('../../../scripts/build/babel-register').registerForMonorepo(); +} + +export * from './getPlatformResolver'; diff --git a/packages/react-native/Libraries/AppDelegate/RCTAppDelegate.h b/packages/react-native/Libraries/AppDelegate/RCTAppDelegate.h index 741dfa92f7fcdc..1466ba2c599923 100644 --- a/packages/react-native/Libraries/AppDelegate/RCTAppDelegate.h +++ b/packages/react-native/Libraries/AppDelegate/RCTAppDelegate.h @@ -59,7 +59,9 @@ NS_ASSUME_NONNULL_BEGIN /// The window object, used to render the UViewControllers @property (nonatomic, strong, nonnull) UIWindow *window; -@property (nonatomic, nullable) RCTBridge *bridge; +/// Store last focused window to properly handle multi-window scenarios +@property (nonatomic, weak, nullable) UIWindow *lastFocusedWindow; +@property (nonatomic, strong, nullable) RCTBridge *bridge; @property (nonatomic, strong, nullable) NSString *moduleName; @property (nonatomic, strong, nullable) NSDictionary *initialProps; @property (nonatomic, strong, nonnull) RCTRootViewFactory *rootViewFactory; diff --git a/packages/react-native/Libraries/AppDelegate/RCTAppDelegate.mm b/packages/react-native/Libraries/AppDelegate/RCTAppDelegate.mm index b02fe560843634..413ac7e96867e7 100644 --- a/packages/react-native/Libraries/AppDelegate/RCTAppDelegate.mm +++ b/packages/react-native/Libraries/AppDelegate/RCTAppDelegate.mm @@ -17,6 +17,7 @@ #import #import "RCTAppDelegate+Protected.h" #import "RCTAppSetupUtils.h" +#import #if RN_DISABLE_OSS_PLUGIN_HEADER #import @@ -47,21 +48,9 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( self.rootViewFactory = [self createRCTRootViewFactory]; - UIView *rootView = [self.rootViewFactory viewWithModuleName:self.moduleName - initialProperties:self.initialProps - launchOptions:launchOptions]; - if (self.newArchEnabled || self.fabricEnabled) { [RCTComponentViewFactory currentComponentViewFactory].thirdPartyFabricComponentsProvider = self; } - [self customizeRootView:(RCTRootView *)rootView]; - - self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; - UIViewController *rootViewController = [self createRootViewController]; - [self setRootView:rootView toRootViewController:rootViewController]; - self.window.rootViewController = rootViewController; - self.window.windowScene.delegate = self; - [self.window makeKeyAndVisible]; return YES; } @@ -90,7 +79,11 @@ - (UIView *)createRootViewWithBridge:(RCTBridge *)bridge BOOL enableFabric = self.fabricEnabled; UIView *rootView = RCTAppSetupDefaultRootView(bridge, moduleName, initProps, enableFabric); +#if TARGET_OS_VISION + rootView.backgroundColor = [UIColor clearColor]; +#else rootView.backgroundColor = [UIColor systemBackgroundColor]; +#endif return rootView; } diff --git a/packages/react-native/Libraries/AppDelegate/RCTRootViewFactory.mm b/packages/react-native/Libraries/AppDelegate/RCTRootViewFactory.mm index dd85650179a025..fb1b81651538dd 100644 --- a/packages/react-native/Libraries/AppDelegate/RCTRootViewFactory.mm +++ b/packages/react-native/Libraries/AppDelegate/RCTRootViewFactory.mm @@ -149,7 +149,11 @@ - (UIView *)viewWithModuleName:(NSString *)moduleName initWithSurface:surface sizeMeasureMode:RCTSurfaceSizeMeasureModeWidthExact | RCTSurfaceSizeMeasureModeHeightExact]; +#if TARGET_OS_VISION + surfaceHostingProxyRootView.backgroundColor = [UIColor clearColor]; +#else surfaceHostingProxyRootView.backgroundColor = [UIColor systemBackgroundColor]; +#endif return surfaceHostingProxyRootView; } @@ -175,7 +179,11 @@ - (UIView *)createRootViewWithBridge:(RCTBridge *)bridge BOOL enableFabric = self->_configuration.fabricEnabled; UIView *rootView = RCTAppSetupDefaultRootView(bridge, moduleName, initProps, enableFabric); +#if TARGET_OS_VISION + rootView.backgroundColor = [UIColor clearColor]; +#else rootView.backgroundColor = [UIColor systemBackgroundColor]; +#endif return rootView; } diff --git a/packages/react-native/Libraries/Components/Keyboard/Keyboard.js b/packages/react-native/Libraries/Components/Keyboard/Keyboard.js index 8aae204e97f328..fd23d472e95a57 100644 --- a/packages/react-native/Libraries/Components/Keyboard/Keyboard.js +++ b/packages/react-native/Libraries/Components/Keyboard/Keyboard.js @@ -15,6 +15,7 @@ import LayoutAnimation from '../../LayoutAnimation/LayoutAnimation'; import dismissKeyboard from '../../Utilities/dismissKeyboard'; import Platform from '../../Utilities/Platform'; import NativeKeyboardObserver from './NativeKeyboardObserver'; +import warnOnce from '../../Utilities/warnOnce'; export type KeyboardEventName = $Keys; @@ -114,6 +115,10 @@ class Keyboard { ); constructor() { + if (Platform.isVision) { + return; + } + this.addListener('keyboardDidShow', ev => { this._currentlyShowing = ev; }); @@ -151,6 +156,14 @@ class Keyboard { listener: (...$ElementType) => mixed, context?: mixed, ): EventSubscription { + if (Platform.isVision) { + warnOnce( + 'Keyboard-unavailable', + 'Keyboard API is not available on visionOS platform. The system displays the keyboard in a separate window, leaving the app’s window unaffected by the keyboard’s appearance and disappearance', + ); + return {remove() {}}; + } + return this._emitter.addListener(eventType, listener); } @@ -160,6 +173,10 @@ class Keyboard { * @param {string} eventType The native event string listeners are watching which will be removed. */ removeAllListeners>(eventType: ?K): void { + if (Platform.isVision) { + return; + } + this._emitter.removeAllListeners(eventType); } diff --git a/packages/react-native/Libraries/Components/Keyboard/KeyboardAvoidingView.js b/packages/react-native/Libraries/Components/Keyboard/KeyboardAvoidingView.js index e26d6771c47209..efea2b6683f40f 100644 --- a/packages/react-native/Libraries/Components/Keyboard/KeyboardAvoidingView.js +++ b/packages/react-native/Libraries/Components/Keyboard/KeyboardAvoidingView.js @@ -24,6 +24,7 @@ import AccessibilityInfo from '../AccessibilityInfo/AccessibilityInfo'; import View from '../View/View'; import Keyboard from './Keyboard'; import * as React from 'react'; +import warnOnce from '../../Utilities/warnOnce'; type Props = $ReadOnly<{| ...ViewProps, @@ -176,6 +177,13 @@ class KeyboardAvoidingView extends React.Component { componentDidMount(): void { if (Platform.OS === 'ios') { + if (Platform.isVision) { + warnOnce( + 'KeyboardAvoidingView-unavailable', + 'KeyboardAvoidingView is not available on visionOS platform. The system displays the keyboard in a separate window, leaving the app’s window unaffected by the keyboard’s appearance and disappearance', + ); + return; + } this._subscriptions = [ Keyboard.addListener('keyboardWillChangeFrame', this._onKeyboardChange), ]; @@ -205,6 +213,16 @@ class KeyboardAvoidingView extends React.Component { onLayout, ...props } = this.props; + + if (Platform.isVision) { + // KeyboardAvoidingView is not supported on VisionOS, so we return a simple View without the onLayout handler + return ( + + {children} + + ); + } + const bottomHeight = enabled === true ? this.state.bottom : 0; switch (behavior) { case 'height': diff --git a/packages/react-native/Libraries/Components/Pressable/Pressable.js b/packages/react-native/Libraries/Components/Pressable/Pressable.js index bd0d6a43a3a666..8b4bc667bdbf38 100644 --- a/packages/react-native/Libraries/Components/Pressable/Pressable.js +++ b/packages/react-native/Libraries/Components/Pressable/Pressable.js @@ -24,6 +24,7 @@ import type { import {PressabilityDebugView} from '../../Pressability/PressabilityDebug'; import usePressability from '../../Pressability/usePressability'; import {type RectOrSize} from '../../StyleSheet/Rect'; +import StyleSheet from '../../StyleSheet/StyleSheet'; import useMergeRefs from '../../Utilities/useMergeRefs'; import View from '../View/View'; import useAndroidRippleForView, { @@ -347,7 +348,10 @@ function Pressable( {...restPropsWithDefaults} {...eventHandlers} ref={mergedRef} - style={typeof style === 'function' ? style({pressed}) : style} + style={[ + styles.pressable, + typeof style === 'function' ? style({pressed}) : style, + ]} collapsable={false}> {typeof children === 'function' ? children({pressed}) : children} {__DEV__ ? : null} @@ -355,6 +359,12 @@ function Pressable( ); } +const styles = StyleSheet.create({ + pressable: { + cursor: 'pointer', + }, +}); + function usePressState(forcePressed: boolean): [boolean, (boolean) => void] { const [pressed, setPressed] = useState(false); return [pressed || forcePressed, setPressed]; diff --git a/packages/react-native/Libraries/Components/Pressable/__tests__/__snapshots__/Pressable-test.js.snap b/packages/react-native/Libraries/Components/Pressable/__tests__/__snapshots__/Pressable-test.js.snap index e348e02347e52b..2f9286b98987d6 100644 --- a/packages/react-native/Libraries/Components/Pressable/__tests__/__snapshots__/Pressable-test.js.snap +++ b/packages/react-native/Libraries/Components/Pressable/__tests__/__snapshots__/Pressable-test.js.snap @@ -31,6 +31,14 @@ exports[` should render as expected: should deep render when mocked onResponderTerminate={[Function]} onResponderTerminationRequest={[Function]} onStartShouldSetResponder={[Function]} + style={ + Array [ + Object { + "cursor": "pointer", + }, + undefined, + ] + } > @@ -67,6 +75,14 @@ exports[` should render as expected: should deep render when not mo onResponderTerminate={[Function]} onResponderTerminationRequest={[Function]} onStartShouldSetResponder={[Function]} + style={ + Array [ + Object { + "cursor": "pointer", + }, + undefined, + ] + } > @@ -115,6 +131,14 @@ exports[` should be disabled when disabled is true: onResponderTerminate={[Function]} onResponderTerminationRequest={[Function]} onStartShouldSetResponder={[Function]} + style={ + Array [ + Object { + "cursor": "pointer", + }, + undefined, + ] + } > @@ -151,6 +175,14 @@ exports[` should be disabled when disabled is true: onResponderTerminate={[Function]} onResponderTerminationRequest={[Function]} onStartShouldSetResponder={[Function]} + style={ + Array [ + Object { + "cursor": "pointer", + }, + undefined, + ] + } > @@ -203,6 +235,14 @@ exports[` should be disable onResponderTerminate={[Function]} onResponderTerminationRequest={[Function]} onStartShouldSetResponder={[Function]} + style={ + Array [ + Object { + "cursor": "pointer", + }, + undefined, + ] + } > @@ -239,6 +279,14 @@ exports[` should be disable onResponderTerminate={[Function]} onResponderTerminationRequest={[Function]} onStartShouldSetResponder={[Function]} + style={ + Array [ + Object { + "cursor": "pointer", + }, + undefined, + ] + } > @@ -293,6 +341,14 @@ exports[` shou onResponderTerminate={[Function]} onResponderTerminationRequest={[Function]} onStartShouldSetResponder={[Function]} + style={ + Array [ + Object { + "cursor": "pointer", + }, + undefined, + ] + } > @@ -329,6 +385,14 @@ exports[` shou onResponderTerminate={[Function]} onResponderTerminationRequest={[Function]} onStartShouldSetResponder={[Function]} + style={ + Array [ + Object { + "cursor": "pointer", + }, + undefined, + ] + } > @@ -391,6 +455,14 @@ exports[` sh onResponderTerminate={[Function]} onResponderTerminationRequest={[Function]} onStartShouldSetResponder={[Function]} + style={ + Array [ + Object { + "cursor": "pointer", + }, + undefined, + ] + } > @@ -427,6 +499,14 @@ exports[` sh onResponderTerminate={[Function]} onResponderTerminationRequest={[Function]} onStartShouldSetResponder={[Function]} + style={ + Array [ + Object { + "cursor": "pointer", + }, + undefined, + ] + } > diff --git a/packages/react-native/Libraries/Components/ScrollView/ScrollView.js b/packages/react-native/Libraries/Components/ScrollView/ScrollView.js index dbe5bf1f218a68..6e8282ec0c3c3e 100644 --- a/packages/react-native/Libraries/Components/ScrollView/ScrollView.js +++ b/packages/react-native/Libraries/Components/ScrollView/ScrollView.js @@ -783,22 +783,24 @@ class ScrollView extends React.Component { this._keyboardMetrics = Keyboard.metrics(); this._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, - ); + if (!Platform.isVision) { + 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, + ); + } this._updateAnimatedNodeAttachment(); } diff --git a/packages/react-native/Libraries/Components/StatusBar/StatusBar.js b/packages/react-native/Libraries/Components/StatusBar/StatusBar.js index 20c2cf49d9fe69..a533c7d63acdbd 100644 --- a/packages/react-native/Libraries/Components/StatusBar/StatusBar.js +++ b/packages/react-native/Libraries/Components/StatusBar/StatusBar.js @@ -12,6 +12,7 @@ import type {ColorValue} from '../../StyleSheet/StyleSheet'; import processColor from '../../StyleSheet/processColor'; import Platform from '../../Utilities/Platform'; +import warnOnce from '../../Utilities/warnOnce'; import NativeStatusBarManagerAndroid from './NativeStatusBarManagerAndroid'; import NativeStatusBarManagerIOS from './NativeStatusBarManagerIOS'; import invariant from 'invariant'; @@ -373,6 +374,13 @@ class StatusBar extends React.Component { _stackEntry = null; componentDidMount() { + if (Platform.isVision) { + warnOnce( + 'StatusBar-unavailable', + 'StatusBar is not available on visionOS platform.', + ); + return; + } // Every time a StatusBar component is mounted, we push it's prop to a stack // and always update the native status bar with the props from the top of then // stack. This allows having multiple StatusBar components and the one that is diff --git a/packages/react-native/Libraries/Components/TextInput/InputAccessoryView.js b/packages/react-native/Libraries/Components/TextInput/InputAccessoryView.js index 43294aab0e4690..4f8a405cf55bfd 100644 --- a/packages/react-native/Libraries/Components/TextInput/InputAccessoryView.js +++ b/packages/react-native/Libraries/Components/TextInput/InputAccessoryView.js @@ -86,7 +86,7 @@ type Props = $ReadOnly<{| |}>; const InputAccessoryView: React.AbstractComponent = (props: Props) => { - if (Platform.OS === 'ios') { + if (Platform.OS === 'ios' && !Platform.isVision) { if (React.Children.count(props.children) === 0) { return null; } diff --git a/packages/react-native/Libraries/Components/Touchable/TouchableHighlight.js b/packages/react-native/Libraries/Components/Touchable/TouchableHighlight.js index 9cdc5f76cb2ea4..60231d8704222f 100644 --- a/packages/react-native/Libraries/Components/Touchable/TouchableHighlight.js +++ b/packages/react-native/Libraries/Components/Touchable/TouchableHighlight.js @@ -329,10 +329,13 @@ class TouchableHighlight extends React.Component { accessibilityElementsHidden={ this.props['aria-hidden'] ?? this.props.accessibilityElementsHidden } - style={StyleSheet.compose( - this.props.style, - this.state.extraStyles?.underlay, - )} + style={[ + styles.touchable, + StyleSheet.compose( + this.props.style, + this.state.extraStyles?.underlay, + ), + ]} onLayout={this.props.onLayout} hitSlop={this.props.hitSlop} hasTVPreferredFocus={this.props.hasTVPreferredFocus} @@ -379,6 +382,12 @@ class TouchableHighlight extends React.Component { } } +const styles = StyleSheet.create({ + touchable: { + cursor: 'pointer', + }, +}); + const Touchable: React.AbstractComponent< $ReadOnly<$Diff|}>>, React.ElementRef, diff --git a/packages/react-native/Libraries/Components/Touchable/TouchableOpacity.js b/packages/react-native/Libraries/Components/Touchable/TouchableOpacity.js index a11eb7cafe3fb9..85e6d7eed82688 100644 --- a/packages/react-native/Libraries/Components/Touchable/TouchableOpacity.js +++ b/packages/react-native/Libraries/Components/Touchable/TouchableOpacity.js @@ -18,6 +18,7 @@ import Pressability, { } from '../../Pressability/Pressability'; import {PressabilityDebugView} from '../../Pressability/PressabilityDebug'; import flattenStyle from '../../StyleSheet/flattenStyle'; +import StyleSheet from '../../StyleSheet/StyleSheet'; import Platform from '../../Utilities/Platform'; import * as React from 'react'; @@ -275,7 +276,7 @@ class TouchableOpacity extends React.Component { accessibilityElementsHidden={ this.props['aria-hidden'] ?? this.props.accessibilityElementsHidden } - style={[this.props.style, {opacity: this.state.anim}]} + style={[styles.touchable, this.props.style, {opacity: this.state.anim}]} nativeID={this.props.id ?? this.props.nativeID} testID={this.props.testID} onLayout={this.props.onLayout} @@ -324,6 +325,12 @@ class TouchableOpacity extends React.Component { } } +const styles = StyleSheet.create({ + touchable: { + cursor: 'pointer', + }, +}); + const Touchable: React.AbstractComponent< Props, React.ElementRef, diff --git a/packages/react-native/Libraries/Components/Touchable/__tests__/__snapshots__/TouchableHighlight-test.js.snap b/packages/react-native/Libraries/Components/Touchable/__tests__/__snapshots__/TouchableHighlight-test.js.snap index 35c845e97493fc..f26560d45d738a 100644 --- a/packages/react-native/Libraries/Components/Touchable/__tests__/__snapshots__/TouchableHighlight-test.js.snap +++ b/packages/react-native/Libraries/Components/Touchable/__tests__/__snapshots__/TouchableHighlight-test.js.snap @@ -19,7 +19,14 @@ exports[`TouchableHighlight renders correctly 1`] = ` onResponderTerminate={[Function]} onResponderTerminationRequest={[Function]} onStartShouldSetResponder={[Function]} - style={Object {}} + style={ + Array [ + Object { + "cursor": "pointer", + }, + Object {}, + ] + } > Touchable @@ -51,6 +58,14 @@ exports[`TouchableHighlight with disabled state should be disabled when disabled onResponderTerminate={[Function]} onResponderTerminationRequest={[Function]} onStartShouldSetResponder={[Function]} + style={ + Array [ + Object { + "cursor": "pointer", + }, + undefined, + ] + } > @@ -80,6 +95,14 @@ exports[`TouchableHighlight with disabled state should be disabled when disabled onResponderTerminate={[Function]} onResponderTerminationRequest={[Function]} onStartShouldSetResponder={[Function]} + style={ + Array [ + Object { + "cursor": "pointer", + }, + undefined, + ] + } > @@ -109,6 +132,14 @@ exports[`TouchableHighlight with disabled state should disable button when acces onResponderTerminate={[Function]} onResponderTerminationRequest={[Function]} onStartShouldSetResponder={[Function]} + style={ + Array [ + Object { + "cursor": "pointer", + }, + undefined, + ] + } > @@ -139,6 +170,14 @@ exports[`TouchableHighlight with disabled state should keep accessibilityState w onResponderTerminate={[Function]} onResponderTerminationRequest={[Function]} onStartShouldSetResponder={[Function]} + style={ + Array [ + Object { + "cursor": "pointer", + }, + undefined, + ] + } > @@ -168,6 +207,14 @@ exports[`TouchableHighlight with disabled state should overwrite accessibilitySt onResponderTerminate={[Function]} onResponderTerminationRequest={[Function]} onStartShouldSetResponder={[Function]} + style={ + Array [ + Object { + "cursor": "pointer", + }, + undefined, + ] + } > diff --git a/packages/react-native/Libraries/Components/Touchable/__tests__/__snapshots__/TouchableOpacity-test.js.snap b/packages/react-native/Libraries/Components/Touchable/__tests__/__snapshots__/TouchableOpacity-test.js.snap index 17f2e7f6f764e0..ca634909ca5afc 100644 --- a/packages/react-native/Libraries/Components/Touchable/__tests__/__snapshots__/TouchableOpacity-test.js.snap +++ b/packages/react-native/Libraries/Components/Touchable/__tests__/__snapshots__/TouchableOpacity-test.js.snap @@ -31,6 +31,7 @@ exports[`TouchableOpacity renders correctly 1`] = ` onStartShouldSetResponder={[Function]} style={ Object { + "cursor": "pointer", "opacity": 1, } } @@ -72,6 +73,7 @@ exports[`TouchableOpacity renders in disabled state when a disabled prop is pass onStartShouldSetResponder={[Function]} style={ Object { + "cursor": "pointer", "opacity": 1, } } @@ -113,6 +115,7 @@ exports[`TouchableOpacity renders in disabled state when a key disabled in acces onStartShouldSetResponder={[Function]} style={ Object { + "cursor": "pointer", "opacity": 1, } } diff --git a/packages/react-native/Libraries/Components/__tests__/__snapshots__/Button-test.js.snap b/packages/react-native/Libraries/Components/__tests__/__snapshots__/Button-test.js.snap index 5b4294e0e8c850..a3efb80e477bfc 100644 --- a/packages/react-native/Libraries/Components/__tests__/__snapshots__/Button-test.js.snap +++ b/packages/react-native/Libraries/Components/__tests__/__snapshots__/Button-test.js.snap @@ -32,6 +32,7 @@ exports[`