diff --git a/.appveyor/config.yml b/.appveyor/config.yml new file mode 100644 index 00000000000000..3753f3eba4783a --- /dev/null +++ b/.appveyor/config.yml @@ -0,0 +1,48 @@ +environment: + ANDROID_HOME: "C:\\android-sdk-windows" + ANDROID_NDK: "C:\\android-sdk-windows\\android-ndk-r17c" + ANDROID_BUILD_VERSION: 28 + ANDROID_TOOLS_VERSION: 28.0.3 + + GRADLE_OPTS: -Dorg.gradle.daemon=false + + SDK_TOOLS_URL: https://dl.google.com/android/repository/sdk-tools-windows-3859397.zip + NDK_TOOLS_URL: https://dl.google.com/android/repository/android-ndk-r17c-windows-x86_64.zip + + matrix: + - nodejs_version: 8 + - nodejs_version: 10 + +install: + # Install Android SDK Tools + - mkdir "%ANDROID_HOME%" + - appveyor DownloadFile "%SDK_TOOLS_URL%" -FileName "%TMP%/sdk-tools.zip" + - 7z x "%TMP%/sdk-tools.zip" -o"%ANDROID_HOME%" > nul + - set PATH=%PATH%;"%ANDROID_HOME%\tools\bin" + + - yes 2> nul | sdkmanager --licenses > nul + - yes 2> nul | sdkmanager "system-images;android-19;google_apis;armeabi-v7a" + - yes 2> nul | sdkmanager "platforms;android-%ANDROID_BUILD_VERSION%" + - yes 2> nul | sdkmanager "build-tools;%ANDROID_TOOLS_VERSION%" + - yes 2> nul | sdkmanager "add-ons;addon-google_apis-google-23" + - yes 2> nul | sdkmanager "extras;android;m2repository" + + - appveyor DownloadFile "%NDK_TOOLS_URL%" -FileName "%TMP%/ndk.zip" + - 7z x "%TMP%/ndk.zip" -o"%ANDROID_HOME%" > nul + + - ps: Install-Product node $env:nodejs_version + - node --version + - yarn --version + - appveyor-retry yarn install + +build_script: + - gradlew.bat RNTester:android:app:assembleRelease + +test_script: + - npm test + +cache: + - node_modules + - "%LOCALAPPDATA%/Yarn" + - "%USERPROFILE%/.gradle/caches" + - "%USERPROFILE%/.gradle/wrapper" diff --git a/.buckconfig b/.buckconfig index a4c6d696ba66b7..de7e1f2e3895c1 100644 --- a/.buckconfig +++ b/.buckconfig @@ -1,13 +1,13 @@ [android] - target = android-26 + target = android-28 [download] max_number_of_retries = 3 [maven_repositories] central = https://repo1.maven.org/maven2 - google = https://maven.google.com + google = https://dl.google.com/dl/android/maven2/ [alias] rntester = //RNTester/android/app:app diff --git a/.circleci/config.yml b/.circleci/config.yml index d80fd6aaf97b02..30616e6dfbd981 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -26,14 +26,6 @@ aliases: - node_modules key: v1-analysis-dependencies-{{ arch }}-{{ checksum "package.json" }}{{ checksum "bots/package.json" }} - - &restore-cache-android-packages - keys: - - v1-android-sdkmanager-packages-api-26-alpha-{{ checksum "scripts/.tests.env" }} - - &save-cache-android-packages - paths: - - /opt/android/sdk - key: v1-android-sdkmanager-packages-api-26-alpha-{{ checksum "scripts/.tests.env" }} - - &restore-cache-gradle keys: - v1-gradle-{{ .Branch }}-{{ checksum "build.gradle" }}-{{ checksum "ReactAndroid/build.gradle" }} @@ -47,23 +39,15 @@ aliases: - ~/.gradle key: v1-gradle-{{ .Branch }}-{{ checksum "build.gradle" }}-{{ checksum "ReactAndroid/build.gradle" }} - - &restore-cache-ndk - keys: - - v3-android-ndk-r17b-{{ checksum "scripts/android-setup.sh" }} - - &save-cache-ndk - paths: - - /opt/ndk - key: v3-android-ndk-r17b-{{ checksum "scripts/android-setup.sh" }} - - &restore-cache-downloads-buck keys: - - v3-buck-v2018.07.23.01-{{ checksum "scripts/circleci/buck_fetch.sh" }}} - - v3-buck-v2018.07.23.01- + - v3-buck-v2019.01.10.01-{{ checksum "scripts/circleci/buck_fetch.sh" }}} + - v3-buck-v2019.01.10.01- - &save-cache-downloads-buck paths: - ~/buck - ~/okbuck - key: v3-buck-v2018.07.23.01-{{ checksum "scripts/circleci/buck_fetch.sh" }} + key: v3-buck-v2019.01.10.01-{{ checksum "scripts/circleci/buck_fetch.sh" }} - &restore-cache-watchman keys: @@ -84,6 +68,14 @@ aliases: - ReactAndroid/build/third-party-ndk key: v1-gradle-{{ checksum "ReactAndroid/build.gradle" }}-{{ checksum "scripts/circleci/gradle_download_deps.sh" }} + - &restore-cache-homebrew + keys: + - v1-homebrew + - &save-cache-homebrew + paths: + - /usr/local/Homebrew + key: v1-homebrew + # Branch Filtering - &filter-only-master-stable branches: @@ -107,11 +99,6 @@ aliases: - /.*-stable/ - gh-pages - # Dependency Management - - &install-ndk - name: Install Android NDK - command: source scripts/android-setup.sh && getAndroidNDK - - &yarn name: Run Yarn command: | @@ -121,24 +108,9 @@ aliases: yarn install --non-interactive --cache-folder ~/.cache/yarn fi - - &install-yarn - name: Install Yarn - command: | - curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add - - echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list - sudo apt-get update && sudo apt-get install yarn - - - &install-node-dependencies - | - npm install --no-package-lock --no-spin --no-progress - - &install-buck name: Install BUCK command: | - if [[ ! -e ~/buck ]]; then - git clone https://github.com/facebook/buck.git ~/buck --branch v2018.07.23.01 --depth=1 - fi - cd ~/buck && ant buck --version # Install related tooling if [[ ! -e ~/okbuck ]]; then @@ -146,38 +118,6 @@ aliases: fi mkdir -p ~/react-native/tooling/junit cp -R ~/okbuck/tooling/junit/* ~/react-native/tooling/junit/. - - - - &install-node - name: Install Node - command: | - curl -sL https://deb.nodesource.com/setup_8.x | sudo -E bash - - sudo apt-get install -y nodejs - - - &create-ndk-directory - name: Create Android NDK Directory - command: | - if [[ ! -e /opt/ndk ]]; then - sudo mkdir /opt/ndk - fi - sudo chown ${USER:=$(/usr/bin/id -run)}:$USER /opt/ndk - - # CircleCI does not support interpolating env variables in the environment - # https://circleci.com/docs/2.0/env-vars/#interpolating-environment-variables-to-set-other-environment-variables - - &configure-android-path - name: Configure Environment Variables - command: | - echo 'export PATH=${ANDROID_NDK}:~/buck/bin:$PATH' >> $BASH_ENV - source $BASH_ENV - - - &install-android-packages - name: Install Android SDK Packages - command: source scripts/android-setup.sh && getAndroidPackages - - - &install-android-build-dependencies - name: Install Android Build Dependencies - command: ./scripts/circleci/apt-get-android-deps.sh - - &validate-android-sdk name: Validate Android SDK Install command: ./scripts/validate-android-sdk.sh @@ -194,23 +134,28 @@ aliases: # eslint sometimes runs into trouble generating the reports - &run-lint-checks name: Lint code - command: | - if [ $((0 % CIRCLE_NODE_TOTAL)) -eq "$CIRCLE_NODE_INDEX" ]; then - scripts/circleci/exec_swallow_error.sh yarn lint --format junit -o ~/react-native/reports/junit/eslint/results.xml - fi + command: scripts/circleci/exec_swallow_error.sh yarn lint --format junit -o ~/react-native/reports/junit/eslint/results.xml - - &run-flow-checks - name: Check for errors in code using Flow - command: | - if [ $((0 % CIRCLE_NODE_TOTAL)) -eq "$CIRCLE_NODE_INDEX" ]; then - yarn flow check - fi + - &run-flow-checks-ios + name: Check for errors in code using Flow (iOS) + command: yarn flow-check-ios + + - &run-flow-checks-android + name: Check for errors in code using Flow (Android) + command: yarn flow-check-android - &run-sanity-checks name: Sanity checks command: | ./scripts/circleci/check_license.sh - ./scripts/circleci/check_cache.sh + ./scripts/circleci/validate_yarn_lockfile.sh + when: always + + - &js-coverage + name: Test coverage + command: | + yarn test --coverage --maxWorkers=2 + cat ./coverage/lcov.info | ./node_modules/.bin/coveralls when: always - &download-dependencies-gradle @@ -242,11 +187,11 @@ aliases: - &build-js-bundle name: Build JavaScript Bundle - command: node local-cli/cli.js bundle --max-workers 2 --platform android --dev true --entry-file ReactAndroid/src/androidTest/js/TestBundle.js --bundle-output ReactAndroid/src/androidTest/assets/AndroidTestBundle.js + command: node cli.js bundle --max-workers 2 --platform android --dev true --entry-file ReactAndroid/src/androidTest/js/TestBundle.js --bundle-output ReactAndroid/src/androidTest/assets/AndroidTestBundle.js - &compile-native-libs name: Compile Native Libs for Unit and Integration Tests - command: ./gradlew :ReactAndroid:packageReactNdkLibsForBuck -Pjobs=$BUILD_THREADS -Pcom.android.build.threadPoolSize=1 + command: ./gradlew :ReactAndroid:packageReactNdkLibsForBuck -Pjobs=$BUILD_THREADS no_output_timeout: 6m - &run-android-unit-tests @@ -263,7 +208,7 @@ aliases: - &build-android-rntester-app name: Build Android RNTester App - command: ./gradlew RNTester:android:app:assembleRelease -Pjobs=$BUILD_THREADS + command: ./gradlew RNTester:android:app:assembleRelease - &collect-android-test-results name: Collect Test Results @@ -282,58 +227,42 @@ aliases: mkdir -p ~/react-native/reports/junit/ mkdir -p ~/react-native/reports/outputs/ - - &boot-simulator-iphone - name: Boot iPhone Simulator + - &brew-install-watchman + name: Install Watchman command: | - if [ $((0 % CIRCLE_NODE_TOTAL)) -eq "$CIRCLE_NODE_INDEX" ]; then - xcrun simctl boot "iPhone 5s" || true - fi + brew install watchman + touch .watchmanconfig - - &boot-simulator-appletv - name: Boot Apple TV Simulator - command: | - if [ $((1 % CIRCLE_NODE_TOTAL)) -eq "$CIRCLE_NODE_INDEX" ]; then - xcrun simctl boot "Apple TV" || true - fi + - &boot-simulator-iphone + name: Boot iPhone Simulator + command: source scripts/.tests.env && xcrun simctl boot "$IOS_DEVICE" || true - &run-objc-ios-tests name: iOS Test Suite - command: | - if [ $((0 % CIRCLE_NODE_TOTAL)) -eq "$CIRCLE_NODE_INDEX" ]; then - ./scripts/objc-test-ios.sh test - fi + command: ./scripts/objc-test-ios.sh test - - &run-objc-tvos-tests - name: tvOS Test Suite + - &display-broken-tests-warning + name: Running broken tests (Ignore any failures past this point) command: | - if [ $((1 % CIRCLE_NODE_TOTAL)) -eq "$CIRCLE_NODE_INDEX" ]; then - ./scripts/objc-test-tvos.sh test - fi + echo 'The following steps are known to be failing on master.' + echo 'They will no-op for most users.' + echo 'PRs that bring these back to green are appreciated.' - &run-podspec-tests - name: Test CocoaPods - command: | - if [ $((2 % CIRCLE_NODE_TOTAL)) -eq "$CIRCLE_NODE_INDEX" ]; then - ./scripts/process-podspecs.sh - fi + name: Test CocoaPods (Disabled) + command: ./scripts/circleci/exec_author_check.sh ./scripts/process-podspecs.sh - &run-e2e-tests - name: End-to-End Test Suite - command: node ./scripts/run-ci-e2e-tests.js --android --ios --tvos --js --retries 3; + name: End-to-End Test Suite (Disabled) + command: ./scripts/circleci/exec_author_check.sh node ./scripts/run-ci-e2e-tests.js --android --ios --js --retries 3; - &run-objc-ios-e2e-tests - name: iOS End-to-End Test Suite - command: | - node ./scripts/run-ci-e2e-tests.js --ios --retries 3; - - - &run-objc-tvos-e2e-tests - name: tvOS End-to-End Test Suite - command: | - node ./scripts/run-ci-e2e-tests.js --tvos --js --retries 3; + name: iOS End-to-End Test Suite (Disabled) + command: ./scripts/circleci/exec_author_check.sh node ./scripts/run-ci-e2e-tests.js --ios --retries 3; - &run-android-e2e-tests - name: Android End-to-End Test Suite - command: node ./scripts/run-ci-e2e-tests.js --android --retries 3; + name: Android End-to-End Test Suite (Disabled) + command: ./scripts/circleci/exec_author_check.sh node ./scripts/run-ci-e2e-tests.js --android --retries 3; - &run-js-e2e-tests name: JavaScript End-to-End Test Suite @@ -347,27 +276,24 @@ defaults: &defaults js_defaults: &js_defaults <<: *defaults docker: - - image: circleci/node:8 - environment: - - PATH: "/opt/yarn/yarn-v1.5.1/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + - image: node:8 android_defaults: &android_defaults <<: *defaults docker: - - image: circleci/android:api-26-node8-alpha + - image: reactnativecommunity/react-native-android:2019-1-19 resource_class: "large" environment: - TERM: "dumb" - ADB_INSTALL_TIMEOUT: 10 - _JAVA_OPTIONS: "-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap" - GRADLE_OPTS: '-Dorg.gradle.daemon=false -Dorg.gradle.jvmargs="-XX:+HeapDumpOnOutOfMemoryError"' - - ANDROID_NDK: '/opt/ndk/android-ndk-r17b' - BUILD_THREADS: 2 macos_defaults: &macos_defaults <<: *defaults macos: - xcode: "9.4.0" + xcode: "10.1.0" version: 2 jobs: @@ -398,7 +324,8 @@ jobs: at: ~/react-native - run: *run-lint-checks - - run: *run-flow-checks + - run: *run-flow-checks-ios + - run: *run-flow-checks-android - store_test_results: path: ~/react-native/reports/junit @@ -417,68 +344,66 @@ jobs: - store_test_results: path: ~/react-native/reports/junit - # Runs unit tests on iOS and Apple TV devices - test_objc: + # Run JavaScript tests on Node LTS + test_node_lts: + <<: *defaults + docker: + - image: node:lts + steps: + - checkout + - run: *setup-artifacts + - run: *yarn + - run: *run-js-tests + - run: yarn run format-check + - store_test_results: + path: ~/react-native/reports/junit + + # Runs unit tests on iOS devices + test_ios: <<: *macos_defaults - parallelism: 3 steps: - attach_workspace: at: ~/react-native - run: *boot-simulator-iphone - - run: *boot-simulator-appletv - - run: brew install watchman + - restore-cache: *restore-cache-homebrew + - run: *brew-install-watchman + - save-cache: *save-cache-homebrew - run: *run-objc-ios-tests - - run: *run-objc-tvos-tests - - run: *run-podspec-tests - store_test_results: path: ~/react-native/reports/junit - # Runs end to end tests - test_end_to_end: + # Runs end to end tests (Detox) + test_detox_end_to_end: <<: *macos_defaults steps: - attach_workspace: at: ~/react-native - - - run: - name: Boot iOS Simulator - command: xcrun simctl boot "iPhone 5s" || true - + - run: xcrun simctl boot "iPhone 5s" || true - run: name: Configure Environment Variables command: | echo 'export PATH=/usr/local/opt/node@8/bin:$PATH' >> $BASH_ENV source $BASH_ENV - - run: name: Install Node 8 command: | brew install node@8 brew link node@8 - node -v - - - run: *run-objc-ios-e2e-tests - - - run: - name: Install Apple Simulator Utilities - command: | brew tap wix/brew brew install applesimutils + node -v + - run: *yarn - run: - name: Build iOS App for Simulator + name: Build iOS app for simulator command: yarn run build-ios-e2e - - run: name: Run Detox Tests command: yarn run test-ios-e2e - - store_test_results: - path: ~/react-native/reports/junit - # Set up an Android environment for downstream jobs test_android: <<: *android_defaults @@ -486,15 +411,6 @@ jobs: - attach_workspace: at: ~/react-native - # Configure Android SDK and related dependencies - - run: *configure-android-path - # Android build deps install from the network faster than cache - - run: *install-android-build-dependencies - - - restore-cache: *restore-cache-android-packages - - run: *install-android-packages - - save-cache: *save-cache-android-packages - # Validate Android SDK installation and packages - run: *validate-android-sdk @@ -504,12 +420,6 @@ jobs: # Keep configuring Android dependencies while AVD boots up - # Install Android NDK - - run: *create-ndk-directory - - restore-cache: *restore-cache-ndk - - run: *install-ndk - - save-cache: *save-cache-ndk - # Install Buck - restore-cache: *restore-cache-downloads-buck - run: *install-buck @@ -542,8 +452,9 @@ jobs: - run: *build-android-rntester-app # Run Android end-to-end tests - # Disabled - # - run: *run-android-e2e-tests + # TODO: Fix these failing tests. + - run: *display-broken-tests-warning + - run: *run-android-e2e-tests # Collect Results - run: *collect-android-test-results @@ -553,70 +464,90 @@ jobs: # Analyze pull request and raise any lint/flow issues. # Issues will be posted to the PR itself via GitHub bots. # This workflow should only fail if the bots fail to run. + # The public github tokens are publicly visible by design analyze_pr: - <<: *js_defaults + <<: *defaults + docker: + - image: node:lts + environment: + - PUBLIC_PULLBOT_GITHUB_TOKEN_A: "a6edf8e8d40ce4e8b11a" + - PUBLIC_PULLBOT_GITHUB_TOKEN_B: "150e1341f4dd9c944d2a" + - PUBLIC_ANALYSISBOT_GITHUB_TOKEN_A: "78a72af35445ca3f8180" + - PUBLIC_ANALYSISBOT_GITHUB_TOKEN_B: "b1a98e0bbd56ff1ccba1" + steps: - - attach_workspace: - at: ~/react-native + - checkout + - run: *setup-artifacts - restore-cache: *restore-cache-analysis - run: *yarn + - run: - name: Install Additional Dependencies + name: Analyze Shell Scripts command: | - if [ -n "$CIRCLE_PR_NUMBER" ]; then - yarn add github@0.2.4 - cd bots - yarn install --non-interactive --cache-folder ~/.cache/yarn - else - echo "Skipping dependency installation." - fi - - save-cache: *save-cache-analysis + echo -e "\\x1B[36mInstalling additional dependencies\\x1B[0m" + apt update && apt install -y shellcheck + yarn add @octokit/rest@15.18.0 + echo -e "\\x1B[36mAnalyzing shell scripts\\x1B[0m"; \ + GITHUB_TOKEN="$PUBLIC_ANALYSISBOT_GITHUB_TOKEN_A""$PUBLIC_ANALYSISBOT_GITHUB_TOKEN_B" \ + GITHUB_OWNER="$CIRCLE_PROJECT_USERNAME" \ + GITHUB_REPO="$CIRCLE_PROJECT_REPONAME" \ + GITHUB_PR_NUMBER="$CIRCLE_PR_NUMBER" \ + ./scripts/circleci/analyze_scripts.sh + when: always - run: - name: Analyze Pull Request + name: Analyze Code command: | - # DANGER_GITHUB_API_TOKEN=React-Linter public_repo access token - if [ -n "$CIRCLE_PR_NUMBER" ]; then - cd bots && DANGER_GITHUB_API_TOKEN="80aa64c50f38a267e9ba""575d41d528f9c234edb8" yarn danger - else - echo "Skipping pull request analysis." - fi + echo -e "\\x1B[36mInstalling additional dependencies\\x1B[0m"; yarn add @octokit/rest@15.18.0 + echo -e "\\x1B[36mAnalyzing code\\x1B[0m"; \ + GITHUB_TOKEN="$PUBLIC_ANALYSISBOT_GITHUB_TOKEN_A""$PUBLIC_ANALYSISBOT_GITHUB_TOKEN_B" \ + GITHUB_OWNER="$CIRCLE_PROJECT_USERNAME" \ + GITHUB_REPO="$CIRCLE_PROJECT_REPONAME" \ + GITHUB_PR_NUMBER="$CIRCLE_PR_NUMBER" \ + ./scripts/circleci/analyze_code.sh when: always + - run: - name: Analyze Code + name: Analyze Pull Request command: | - # GITHUB_TOKEN=eslint-bot public_repo access token - if [ -n "$CIRCLE_PR_NUMBER" ]; then - GITHUB_TOKEN="af6ef0d15709bc91d""06a6217a5a826a226fb57b7" CI_USER=$CIRCLE_PROJECT_USERNAME CI_REPO=$CIRCLE_PROJECT_REPONAME PULL_REQUEST_NUMBER=$CIRCLE_PR_NUMBER scripts/circleci/analyze_code.sh - else - echo "Skipping code analysis." - fi + echo -e "\\x1B[36mInstalling additional dependencies\\x1B[0m" + cd bots + yarn install --non-interactive --cache-folder ~/.cache/yarn + echo -e "\\x1B[36mAnalyzing pull request\\x1B[0m"; \ + DANGER_GITHUB_API_TOKEN="$PUBLIC_PULLBOT_GITHUB_TOKEN_A""$PUBLIC_PULLBOT_GITHUB_TOKEN_B" \ + yarn danger ci --use-github-checks when: always + - save-cache: *save-cache-analysis + + # Test Coverage + js_coverage: + <<: *js_defaults + environment: + - CI_BRANCH: $CIRCLE_BRANCH + - CI_PULL_REQUEST: $CIRCLE_PULL_REQUEST + - CI_BUILD_NUMBER: $CIRCLE_BUILD_NUM + - CI_BUILD_URL: $CIRCLE_BUILD_URL + steps: + - checkout + - restore-cache: *restore-yarn-cache + - run: *yarn + - run: *js-coverage + - store_artifacts: + path: ~/react-native/coverage/ # Publishes new version onto npm # Only works on stable branches when a properly tagged commit is pushed publish_npm_package: <<: *android_defaults steps: - - attach_workspace: - at: ~/react-native - - # Configure Android SDK and related dependencies - - run: *configure-android-path - - run: *install-android-build-dependencies - - - restore-cache: *restore-cache-android-packages - - run: *install-android-packages + - checkout - # Install Android NDK - - run: *create-ndk-directory - - restore-cache: *restore-cache-ndk - - run: *install-ndk + - restore-cache: *restore-yarn-cache + - run: *yarn # Fetch dependencies using Buck - restore-cache: *restore-cache-downloads-buck - - run: *install-buck - run: *download-dependencies-buck # Fetch dependencies using Gradle @@ -627,13 +558,19 @@ jobs: - run: *yarn - run: - name: Publish React Native Package + name: Authenticate with npm + command: echo "//registry.npmjs.org/:_authToken=${CIRCLE_NPM_TOKEN}" > ~/.npmrc + + - run: + name: Authenticate git user command: | - echo "//registry.npmjs.org/:_authToken=${CIRCLE_NPM_TOKEN}" > ~/.npmrc - git config --global user.email "reactjs-bot@users.noreply.github.com" + git config --global user.email "react-native-bot@users.noreply.github.com" git config --global user.name "npm Deployment Script" - echo "machine github.com login reactjs-bot password $GITHUB_TOKEN" > ~/.netrc - node ./scripts/publish-npm.js + echo "machine github.com login react-native-bot password $GITHUB_TOKEN" > ~/.netrc + + - run: + name: Publish React Native Package + command: node ./scripts/publish-npm.js # Workflows enables us to run multiple jobs in parallel workflows: @@ -663,42 +600,43 @@ workflows: requires: - checkout_code - # Test iOS & tvOS - - test_objc: + # Test iOS + - test_ios: filters: *filter-ignore-gh-pages requires: - checkout_code - # End-to-end tests - - test_end_to_end: + - test_detox_end_to_end: filters: *filter-ignore-gh-pages requires: - checkout_code + # Tooling Compatibility Checks + - test_node_lts: + filters: *filter-ignore-gh-pages + + releases: + jobs: # Only runs on vX.X.X tags if all tests are green - publish_npm_package: filters: + # ignore any commit on any branch by default branches: - only: - - /.*-stable/ + ignore: /.*/ + # only act on version tags tags: only: /v[0-9]+(\.[0-9]+)*(\-rc(\.[0-9]+)?)?/ - requires: - - test_javascript - - test_objc - - test_android - - test_end_to_end - - analyze - # Only runs on PRs - analyze: + analysis: jobs: - # Checkout repo and run Yarn - - checkout_code: - filters: *filter-ignore-master-stable - - # Run code checks + # Run code checks on PRs from forks - analyze_pr: - filters: *filter-ignore-master-stable - requires: - - checkout_code + filters: + branches: + only: /^pull\/.*$/ + + # Gather coverage on master + - js_coverage: + filters: + branches: + only: master diff --git a/.clang-format b/.clang-format new file mode 100644 index 00000000000000..bc20b078f0eaf9 --- /dev/null +++ b/.clang-format @@ -0,0 +1,91 @@ +--- +AccessModifierOffset: -1 +AlignAfterOpenBracket: AlwaysBreak +AlignConsecutiveAssignments: false +AlignConsecutiveDeclarations: false +AlignEscapedNewlinesLeft: true +AlignOperands: false +AlignTrailingComments: false +AllowAllParametersOfDeclarationOnNextLine: false +AllowShortBlocksOnASingleLine: false +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: Empty +AllowShortIfStatementsOnASingleLine: false +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: true +AlwaysBreakTemplateDeclarations: true +BinPackArguments: false +BinPackParameters: false +BraceWrapping: + AfterClass: false + AfterControlStatement: false + AfterEnum: false + AfterFunction: false + AfterNamespace: false + AfterObjCDeclaration: false + AfterStruct: false + AfterUnion: false + BeforeCatch: false + BeforeElse: false + IndentBraces: false +BreakBeforeBinaryOperators: None +BreakBeforeBraces: Attach +BreakBeforeTernaryOperators: true +BreakConstructorInitializersBeforeComma: false +BreakAfterJavaFieldAnnotations: false +BreakStringLiterals: false +ColumnLimit: 80 +CommentPragmas: '^ IWYU pragma:' +ConstructorInitializerAllOnOneLineOrOnePerLine: true +ConstructorInitializerIndentWidth: 4 +ContinuationIndentWidth: 4 +Cpp11BracedListStyle: true +DerivePointerAlignment: false +DisableFormat: false +ForEachMacros: [ FOR_EACH_RANGE, FOR_EACH, ] +IncludeCategories: + - Regex: '^<.*\.h(pp)?>' + Priority: 1 + - Regex: '^<.*' + Priority: 2 + - Regex: '.*' + Priority: 3 +IndentCaseLabels: true +IndentWidth: 2 +IndentWrappedFunctionNames: false +KeepEmptyLinesAtTheStartOfBlocks: false +MacroBlockBegin: '' +MacroBlockEnd: '' +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: None +ObjCBlockIndentWidth: 2 +ObjCSpaceAfterProperty: true +ObjCSpaceBeforeProtocolList: true +PenaltyBreakBeforeFirstCallParameter: 1 +PenaltyBreakComment: 300 +PenaltyBreakFirstLessLess: 120 +PenaltyBreakString: 1000 +PenaltyExcessCharacter: 1000000 +PenaltyReturnTypeOnItsOwnLine: 200 +PointerAlignment: Right +ReflowComments: true +SortIncludes: true +SpaceAfterCStyleCast: false +SpaceBeforeAssignmentOperators: true +SpaceBeforeParens: ControlStatements +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 1 +SpacesInAngles: false +SpacesInContainerLiterals: true +SpacesInCStyleCastParentheses: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +Standard: Cpp11 +TabWidth: 8 +UseTab: Never +--- +Language: ObjC +ColumnLimit: 120 +BreakBeforeBraces: WebKit +... diff --git a/.editorconfig b/.editorconfig index 4cde30709106fc..dadac3a2b6d50b 100644 --- a/.editorconfig +++ b/.editorconfig @@ -12,3 +12,6 @@ indent_size = 2 [*.gradle] indent_size = 4 + +[BUCK] +indent_size = 4 diff --git a/.eslintignore b/.eslintignore index 698b5357d68697..8fe49b19c284a8 100644 --- a/.eslintignore +++ b/.eslintignore @@ -7,4 +7,4 @@ Libraries/Renderer/* pr-inactivity-bookmarklet.js question-bookmarklet.js flow/ -danger/ +bots/node_modules \ No newline at end of file diff --git a/.eslintrc b/.eslintrc index 01842b1bcd0e5f..6fef2c2c1d1538 100644 --- a/.eslintrc +++ b/.eslintrc @@ -5,7 +5,6 @@ "env": { "es6": true, - "jest": true, }, "plugins": [ @@ -13,8 +12,9 @@ "flowtype", "prettier", "react", + "react-hooks", "react-native", - "jest" + "jest", ], // Map from global var to bool specifying if it can be redefined @@ -37,7 +37,6 @@ "fetch": false, "FormData": false, "global": false, - "jest": false, "Map": true, "module": false, "navigator": false, @@ -52,8 +51,6 @@ "setTimeout": false, "window": false, "XMLHttpRequest": false, - "pit": false, - "jasmine": true }, "rules": { @@ -65,6 +62,7 @@ "no-constant-condition": 0, // disallow use of constant expressions in conditions "no-control-regex": 1, // disallow control characters in regular expressions "no-debugger": 1, // disallow use of debugger + "no-dupe-class-members": 2, // Disallow duplicate name in class members "no-dupe-keys": 2, // disallow duplicate keys when creating object literals "no-empty": 0, // disallow empty statements "no-ex-assign": 1, // disallow assigning to the exception in a catch block @@ -243,6 +241,10 @@ "react/self-closing-comp": 1, "react/wrap-multilines": 0, + // React-Hooks Plugin + // The following rules are made available via `eslint-plugin-react-hooks` + "react-hooks/rules-of-hooks": "error", + // React-Native Plugin // The following rules are made available via `eslint-plugin-react-native` @@ -259,16 +261,38 @@ "overrides": [ { "files": [ - "local-cli/**/*.js", + "packages/react-native-symbolicate/**/*.js", ], - "rules": { - "lint/extra-arrow-initializer": 0, - "no-alert": 0, - "no-console-disallow": 0, - }, "env": { "node": true, }, }, + { + "files": [ + "**/__fixtures__/**/*.js", + "**/__mocks__/**/*.js", + "**/__tests__/**/*.js", + "jest/**/*.js", + "RNTester/**/*.js", + ], + "globals": { + // Expose some Jest globals for test helpers + "afterAll": true, + "afterEach": true, + "beforeAll": true, + "beforeEach": true, + "expect": true, + "jest": true, + }, + }, + { + "files": [ + "**/__tests__/**/*-test.js", + ], + "env": { + "jasmine": true, + "jest": true, + }, + }, ], } diff --git a/.flowconfig b/.flowconfig index b402919a3741bd..9066cd3e0f96d3 100644 --- a/.flowconfig +++ b/.flowconfig @@ -3,7 +3,7 @@ .*/*[.]android.js ; Ignore templates for 'react-native init' -.*/local-cli/templates/.* +/template/.* ; Ignore the Dangerfile /bots/dangerfile.js @@ -22,24 +22,25 @@ ; Ignore polyfills .*/Libraries/polyfills/.* -; Ignore metro -.*/node_modules/metro/.* - ; These should not be required directly -; require from fbjs/lib instead: require('fbjs/lib/invariant') -.*/node_modules/invariant/.* +; require from fbjs/lib instead: require('fbjs/lib/warning') .*/node_modules/warning/.* +[untyped] +.*/node_modules/@react-native-community/cli/.*/.* + [include] [libs] Libraries/react-native/react-native-interface.js flow/ -flow-github/ [options] emoji=true +esproposal.optional_chaining=enable +esproposal.nullish_coalescing=enable + module.system=haste module.system.haste.use_name_reducers=true # keep the following in sync with server/haste/hasteImpl.js @@ -53,10 +54,10 @@ module.system.haste.name_reducers='^\(.*\)\.android$' -> '\1' module.system.haste.name_reducers='^\(.*\)\.native$' -> '\1' module.system.haste.paths.blacklist=.*/__tests__/.* module.system.haste.paths.blacklist=.*/__mocks__/.* -module.system.haste.paths.blacklist=/Libraries/Animated/src/polyfills/.* module.system.haste.paths.whitelist=/Libraries/.* module.system.haste.paths.whitelist=/RNTester/.* module.system.haste.paths.whitelist=/IntegrationTests/.* +module.system.haste.paths.blacklist=/Libraries/react-native/react-native-implementation.js module.system.haste.paths.blacklist=/Libraries/Animated/src/polyfills/.* munge_underscores=true @@ -68,24 +69,24 @@ suppress_type=$FlowFixMe suppress_type=$FlowFixMeProps suppress_type=$FlowFixMeState -suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(\\)? *\\(site=[a-z,_]*[react_native_oss|react_native_fb][a-z,_]*\\)?)\\) -suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(\\)? *\\(site=[a-z,_]*[react_native_oss|react_native_fb][a-z,_]*\\)?)\\)?:? #[0-9]+ +suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(\\)? *\\(site=[a-z,_]*react_native\\(_ios\\)?_\\(oss\\|fb\\)[a-z,_]*\\)?)\\) +suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(\\)? *\\(site=[a-z,_]*react_native\\(_ios\\)?_\\(oss\\|fb\\)[a-z,_]*\\)?)\\)?:? #[0-9]+ suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy suppress_comment=\\(.\\|\n\\)*\\$FlowExpectedError [lints] -all=warn - -# There is an ESLint rule for this -unclear-type=off - -sketchy-null=off sketchy-null-number=warn sketchy-null-mixed=warn - -# This is noisy for now. We *do* still want to warn on importing types -# from untyped files, which is covered by untyped-type-import -untyped-import=off +sketchy-number=warn +untyped-type-import=warn +nonstrict-import=warn +deprecated-type=warn +unsafe-getters-setters=warn +inexact-spread=warn +unnecessary-invariant=warn +deprecated-call-syntax=warn +signature-verification-failure=warn +deprecated-utility=error [strict] deprecated-type @@ -97,4 +98,4 @@ untyped-import untyped-type-import [version] -^0.77.0 +^0.92.0 diff --git a/.flowconfig.android b/.flowconfig.android new file mode 100644 index 00000000000000..c2e4cf6410bd68 --- /dev/null +++ b/.flowconfig.android @@ -0,0 +1,101 @@ +[ignore] +; We fork some components by platform +.*/*[.]ios.js + +; Ignore templates for 'react-native init' +/template/.* + +; Ignore the Dangerfile +/bots/dangerfile.js + +; Ignore "BUCK" generated dirs +/\.buckd/ + +; Ignore unexpected extra "@providesModule" +.*/node_modules/.*/node_modules/fbjs/.* + +; Ignore duplicate module providers +; For RN Apps installed via npm, "Libraries" folder is inside +; "node_modules/react-native" but in the source repo it is in the root +.*/Libraries/react-native/React.js + +; Ignore polyfills +.*/Libraries/polyfills/.* + +; These should not be required directly +; require from fbjs/lib instead: require('fbjs/lib/warning') +.*/node_modules/warning/.* + +[untyped] +.*/node_modules/@react-native-community/cli/.*/.* + +[include] + +[libs] +Libraries/react-native/react-native-interface.js +flow/ + +[options] +emoji=true + +esproposal.optional_chaining=enable +esproposal.nullish_coalescing=enable + +module.system=haste +module.system.haste.use_name_reducers=true +# keep the following in sync with server/haste/hasteImpl.js +# get basename +module.system.haste.name_reducers='^.*/\([a-zA-Z0-9$_.-]+\.js\(\.flow\)?\)$' -> '\1' +# strip .js or .js.flow suffix +module.system.haste.name_reducers='^\(.*\)\.js\(\.flow\)?$' -> '\1' +# strip .android suffix +module.system.haste.name_reducers='^\(.*\)\.android$' -> '\1' +module.system.haste.name_reducers='^\(.*\)\.ios$' -> '\1' +module.system.haste.name_reducers='^\(.*\)\.native$' -> '\1' +module.system.haste.paths.blacklist=.*/__tests__/.* +module.system.haste.paths.blacklist=.*/__mocks__/.* +module.system.haste.paths.whitelist=/Libraries/.* +module.system.haste.paths.whitelist=/RNTester/.* +module.system.haste.paths.whitelist=/IntegrationTests/.* +module.system.haste.paths.blacklist=/Libraries/react-native/react-native-implementation.js +module.system.haste.paths.blacklist=/Libraries/Animated/src/polyfills/.* + +munge_underscores=true + +module.name_mapper='^[./a-zA-Z0-9$_-]+\.\(bmp\|gif\|jpg\|jpeg\|png\|psd\|svg\|webp\|m4v\|mov\|mp4\|mpeg\|mpg\|webm\|aac\|aiff\|caf\|m4a\|mp3\|wav\|html\|pdf\)$' -> 'RelativeImageStub' + +suppress_type=$FlowIssue +suppress_type=$FlowFixMe +suppress_type=$FlowFixMeProps +suppress_type=$FlowFixMeState + +suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(\\)? *\\(site=[a-z,_]*react_native\\(_android\\)?_\\(oss\\|fb\\)[a-z,_]*\\)?)\\) +suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(\\)? *\\(site=[a-z,_]*react_native\\(_android\\)?_\\(oss\\|fb\\)[a-z,_]*\\)?)\\)?:? #[0-9]+ +suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy +suppress_comment=\\(.\\|\n\\)*\\$FlowExpectedError + +[lints] +sketchy-null-number=warn +sketchy-null-mixed=warn +sketchy-number=warn +untyped-type-import=warn +nonstrict-import=warn +deprecated-type=warn +unsafe-getters-setters=warn +inexact-spread=warn +unnecessary-invariant=warn +deprecated-call-syntax=warn +signature-verification-failure=warn +deprecated-utility=error + +[strict] +deprecated-type +nonstrict-import +sketchy-null +unclear-type +unsafe-getters-setters +untyped-import +untyped-type-import + +[version] +^0.92.0 diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index b9d084bf849cf8..6a98f2155a0da2 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -7,8 +7,5 @@ React/Views/* @shergin React/Modules/* @shergin React/CxxBridge/* @mhorowitz ReactAndroid/src/main/java/com/facebook/react/animated/* @janicduplessis -**/*.md @hramos -package.json @hramos -local-cli/core/* @grabbou @kureev -local-cli/link/* @grabbou @kureev -local-cli/unlink/* @grabbou @kureev +**/*.md @hramos @cpojer +package.json @hramos @cpojer diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index d31f3a36ccac03..3a342453133dc8 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,3 +1,4 @@ -GitHub Issues in the `facebook/react-native` repository are used exclusively for tracking bugs in React Native. +šŸ‘‰ Please follow one of these issue templates: +- https://github.com/facebook/react-native/issues/new/choose -Please take a look at the issue templates at https://github.com/facebook/react-native/issues/new/choose before submitting a new issue. Following one of the issue templates will ensure maintainers can route your request efficiently. Thanks! +Note: to keep the backlog clean and actionable, issues may be immediately closed if they do not follow one of the above issue templates. diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 85c66cde5f6281..85d815179a6ce3 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,18 +1,35 @@ --- name: šŸ› Bug Report -about: Report a reproducible bug or regression in the core React Native library. +about: You want to report a reproducible bug or regression in React Native. +labels: "Type: Bug Report" --- - - - [ ] Review the documentation: https://facebook.github.io/react-native - - [ ] Search for existing issues: https://github.com/facebook/react-native/issues - - [ ] Use the latest React Native release: https://github.com/facebook/react-native/releases +## šŸ› Bug Report + -## Environment -Run `react-native info` in your terminal and paste its contents here. +## To Reproduce + + +## Expected Behavior + -## Description -Describe your issue in detail. Include screenshots if needed. If this is a regression, let us know. +## Code Example + + +## Environment + diff --git a/.github/ISSUE_TEMPLATE/discussion.md b/.github/ISSUE_TEMPLATE/discussion.md index 4bd6a92cfc6e71..2e6be6c030d610 100644 --- a/.github/ISSUE_TEMPLATE/discussion.md +++ b/.github/ISSUE_TEMPLATE/discussion.md @@ -1,17 +1,18 @@ --- -name: šŸ—£ Start a Discussion -about: Use https://github.com/react-native-community/discussions-and-proposals to propose changes or discuss feature requests. +name: šŸš€ Discussion +about: You have an idea that could make React Native better, or you want to discuss some aspect of the framework. +labels: "Type: Discussion" --- -Please use https://github.com/react-native-community/discussions-and-proposals to propose changes or discuss feature requests. +If you want to participate in casual discussions about the use of React Native, consider participating in one of the following forums: +- Discord Community: https://discord.gg/0ZcbPKXt5bZjGY5n +- Spectrum Chat: https://spectrum.chat/react-native +- Facebook Group: https://www.facebook.com/groups/react.native.community -We kindly ask that issues of this type are kept in that dedicated repo, to ensure bug reports and regressions are given the priority they require. +For a full list of community resources: +- http://facebook.github.io/react-native/help -Maintainers may flag an issue for Stack Overflow or kindly ask you to move the discussion to https://github.com/react-native-community/discussions-and-proposals at their own discretion. - ---- - -# For Discussion - - +If you'd like to discuss topics related to the future of React Native, please check out the discussions and proposals repo: +- https://github.com/react-native-community/discussions-and-proposals +### Please note that discussions opened as issues in the core React Native repository will be closed. \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/documentation.md b/.github/ISSUE_TEMPLATE/documentation.md index 1a4751ee5a9998..acc4af60f9d47b 100644 --- a/.github/ISSUE_TEMPLATE/documentation.md +++ b/.github/ISSUE_TEMPLATE/documentation.md @@ -1,11 +1,12 @@ --- -name: šŸ“– Documentation Issue -about: Report issues with the docs at https://github.com/facebook/react-native-website/issues. - +name: šŸ“ƒ Documentation Bug +about: You want to report something that is wrong or missing from the documentation. +labels: "Type: Docs" --- ---------------^ Click "Preview" for a nicer view! - -GitHub Issues in the `facebook/react-native` repository are used exclusively for tracking bugs in React Native. +The React Native website is hosted on a separate repository. You may let the +team know about any issues with the documentation by opening an issue there: +- https://github.com/facebook/react-native-website +- https://github.com/facebook/react-native-website/issues -If you would like to report an issue in the React Native documentation, or anything related to the [React Native website](http://facebook.github.io/react-native), please visit https://github.com/facebook/react-native-website/issues. +### Please do not open a documentation issue in the core React Native repository. \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md index cc203120f24559..b5fc4bdbf0d3f0 100644 --- a/.github/ISSUE_TEMPLATE/question.md +++ b/.github/ISSUE_TEMPLATE/question.md @@ -1,21 +1,15 @@ --- -name: šŸ’¬ Question -about: If you need help with your React Native app, the right place to go depends on the type of help that you need. - +name: šŸ’¬ Questions and Help +about: You need help writing your React Native app. +labels: "Type: Question" --- ---------------^ Click "Preview" for a nicer view! - -GitHub Issues in the `facebook/react-native` repository are used exclusively for tracking bugs in React Native. GitHub may not be the ideal place to ask questions about using React Native, but we have compiled a list of resources that should help. - -### Get help with your React Native app or ask code-level questions - -Many members of the community use Stack Overflow to ask questions. Read through the [existing questions](http://stackoverflow.com/questions/tagged/react-native?sort=frequent) tagged with **react-native** or [ask your own](http://stackoverflow.com/questions/ask?tags=react-native)! - -### Talk about best practices or propose changes to React Native +GitHub Issues in the `facebook/react-native` repository are used exclusively for tracking bugs in the React Native framework. Please do not submit support requests through GitHub. -For longer-form conversations about React Native, weā€™ve set up a [discussion forum at **discuss.reactjs.org**](https://discuss.reactjs.org/t/welcome-react-native-community-group/10239). This forum is a great place for discussion about best practices and application architecture as well as the future of React Native. +For questions or help, please see: +- The React Native help page: http://facebook.github.io/react-native/help +- The React Native channel in Reactiflux: https://discord.gg/0ZcbPKXt5bZjGY5n +- The react-native tag on Stack Overflow: http://stackoverflow.com/questions/tagged/react-native -### Chat with React and React Native community members -If you need an answer right away, check out the [Reactiflux Discord](https://discord.gg/0ZcbPKXt5bZjGY5n) community. There are usually a number of React Native experts there who can help out or point you to somewhere you might want to look. +### Please note that this issue tracker is not a help forum and requests for help will be closed. \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/regression.md b/.github/ISSUE_TEMPLATE/regression.md new file mode 100644 index 00000000000000..b54db1473b7791 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/regression.md @@ -0,0 +1,42 @@ +--- +name: šŸ’„ Regression Report +about: You want to report unexpected behavior that worked in previous releases. +labels: "Type: Bug Report", "Impact: Regression" +--- + +## šŸ’„ Regression Report + + +## Last working version + +Worked up to version: + +Stopped working in version: + +## To Reproduce + + + +## Expected Behavior + + + +## Code Example + + +## Environment + diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 2ae837de23e7e3..80b2f5c1bffda9 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,37 +1,15 @@ -Thank you for sending the PR! We appreciate you spending the time to work on these changes. -Help us understand your motivation by explaining why you decided to make this change. + -If this PR fixes an issue, type "Fixes #issueNumber" to automatically close the issue when the PR is merged. +## Summary -Test Plan: ----------- -Write your test plan here. If you changed any code, please provide us with clear instructions on how you verified your changes work. Bonus points for screenshots and videos! + -Release Notes: --------------- -Help reviewers and the release process by writing your own release notes. See below for an example. +## Changelog -[CATEGORY] [TYPE] [LOCATION] - Message + - + diff --git a/.github/stale.yml b/.github/stale.yml index 538514d6469695..bf9e3a327e57dc 100644 --- a/.github/stale.yml +++ b/.github/stale.yml @@ -2,14 +2,16 @@ # Number of days of inactivity before an issue becomes stale daysUntilStale: 90 # Number of days of inactivity before a stale issue is closed -daysUntilClose: 30 +daysUntilClose: 7 # Issues with these labels will never be considered stale exemptLabels: - Good first issue - - For Discussion + - "Type: Discussion" + - Partner - Core Team - "Help Wanted :octocat:" - - ":warning:Regression" + - "Impact: Regression" + - "Resolution: PR Submitted" # Label to use when marking an issue as stale staleLabel: Stale # Comment to post when marking an issue as stale. Set to `false` to disable diff --git a/.gitignore b/.gitignore index 50a3ed6420a2e6..4fb4a25d15f722 100644 --- a/.gitignore +++ b/.gitignore @@ -51,10 +51,6 @@ node_modules *.log .nvm /bots/node_modules/ - -# TODO: Check in yarn.lock in open source. Right now we need to keep it out -# from the GitHub repo as importing it might conflict with internal workspaces -yarn.lock package-lock.json # OS X diff --git a/.npmignore b/.npmignore deleted file mode 100644 index 2e0f2c1f8c6230..00000000000000 --- a/.npmignore +++ /dev/null @@ -1,3 +0,0 @@ -# rnpm -/local-cli/rnpm -/local-cli/server/middleware/heapCapture/bundle.js diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 062c7ca1df5c2d..0f7ad8bfc173ea 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -1,43 +1,5 @@ -# [Open Source Code of Conduct](https://code.facebook.com/codeofconduct) +# Code of Conduct -This code of conduct outlines our expectations for participants within the **Facebook Open Source** community, as well as steps to reporting unacceptable behavior. We are committed to providing a welcoming and inspiring community for all and expect our code of conduct to be honored. Anyone who violates this code of conduct may be banned from the community. - -Our open source community strives to: - -* **Be friendly and patient.** -* **Be welcoming:** We strive to be a community that welcomes and supports people of all backgrounds and identities. This includes, but is not limited to members of any race, ethnicity, culture, national origin, colour, immigration status, social and economic class, educational level, sex, sexual orientation, gender identity and expression, age, size, family status, political belief, religion, and mental and physical ability. -* **Be considerate:** Your work will be used by other people, and you in turn will depend on the work of others. Any decision you take will affect users and colleagues, and you should take those consequences into account when making decisions. Remember that weā€™re a world-wide community, so you might not be communicating in someone elseā€™s primary language. -* **Be respectful:** Not all of us will agree all the time, but disagreement is no excuse for poor behavior and poor manners. We might all experience some frustration now and then, but we cannot allow that frustration to turn into a personal attack. Itā€™s important to remember that a community where people feel uncomfortable or threatened is not a productive one. -* **Be careful in the words that you choose:** we are a community of professionals, and we conduct ourselves professionally. Be kind to others. Do not insult or put down other participants. Harassment and other exclusionary behavior arenā€™t acceptable. This includes, but is not limited to: - * Violent threats or language directed against another person. - * Discriminatory jokes and language. - * Posting sexually explicit or violent material. - * Posting (or threatening to post) other peopleā€™s personally identifying information (ā€œdoxingā€). - * Personal insults, especially those using racist or sexist terms. - * Unwelcome sexual attention. - * Advocating for, or encouraging, any of the above behavior. - * Repeated harassment of others. In general, if someone asks you to stop, then stop. -* **When we disagree, try to understand why:** Disagreements, both social and technical, happen all the time. It is important that we resolve disagreements and differing views constructively. -* **Remember that weā€™re different.** The strength of our community comes from its diversity, people from a wide range of backgrounds. Different people have different perspectives on issues. Being unable to understand why someone holds a viewpoint doesnā€™t mean that theyā€™re wrong. Donā€™t forget that it is human to err and blaming each other doesnā€™t get us anywhere. Instead, focus on helping to resolve issues and learning from mistakes. - -This code is not exhaustive or complete. It serves to distill our common understanding of a collaborative, shared environment, and goals. We expect it to be followed in spirit as much as in the letter. - -## Diversity Statement - -We encourage everyone to participate and are committed to building a community for all. Although we may not be able to satisfy everyone, we all agree that everyone is equal. Whenever a participant has made a mistake, we expect them to take responsibility for it. If someone has been harmed or offended, it is our responsibility to listen carefully and respectfully, and do our best to right the wrong. - -Although this list cannot be exhaustive, we explicitly honor diversity in age, gender, gender identity or expression, culture, ethnicity, language, national origin, political beliefs, profession, race, religion, sexual orientation, socioeconomic status, and technical ability. We will not tolerate discrimination based on any of the protected characteristics above, including participants with disabilities. - -## Reporting Issues - -If you experience or witness unacceptable behaviorā€”or have any other concernsā€”please report it by contacting us via opensource@fb.com. All reports will be handled with discretion. In your report please include: - -* Your contact information. -* Names (real, nicknames, or pseudonyms) of any individuals involved. If there are additional witnesses, please include them as well. Your account of what occurred, and if you believe the incident is ongoing. If there is a publicly available record (e.g. a mailing list archive or a public IRC logger), please include a link. -* Any additional information that may be helpful. - -After filing a report, a representative will contact you personally. If the person who is harassing you is part of the response team, they will recuse themselves from handling your incident. A representative will then review the incident, follow up with any additional questions, and make a decision as to how to respond. We will respect confidentiality requests for the purpose of protecting victims of abuse. - -Anyone asked to stop unacceptable behavior is expected to comply immediately. If an individual engages in unacceptable behavior, the representative may take any action they deem appropriate, up to and including a permanent ban from our community without warning. - -_This Code Of Conduct follows the [template](http://todogroup.org/opencodeofconduct/) established by the [TODO Group](http://todogroup.org/)._ +Facebook has adopted a Code of Conduct that we expect project participants to adhere to. +Please read the [full text](https://code.fb.com/codeofconduct/) +so that you can understand what actions will and will not be tolerated. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 25ba2d5c1e1d51..743a6db7f3cecc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,192 +1,41 @@ # Contributing to React Native - -React Native is one of Facebook's first open source projects that is both under very active development and is also being used to ship code to everybody using Facebook's mobile apps. If you're interested in contributing to React Native, hopefully this document makes the process for contributing clear. +We want to make contributing to this project as easy and transparent as possible. Read on to learn more about our development process and how to propose bug fixes and improvements. The [How to Contribute](https://facebook.github.io/react-native/docs/contributing.html) guide on the website goes into more detail for each of these areas. -## [Code of Conduct](https://code.facebook.com/codeofconduct) +## Our Development Process -Facebook has adopted a Code of Conduct that we expect project participants to adhere to. Please read [the full text](https://code.facebook.com/codeofconduct) so that you can understand what actions will and will not be tolerated. +Most changes from engineers at Facebook will sync to [GitHub](https://github.com/facebook/react-native) through a bridge with Facebook's internal source control. Changes from the community are handled through GitHub pull requests. Once a change made on GitHub is approved, it will first be imported into Facebook's internal source control. The change will eventually sync back to GitHub as a single commit once it has passed Facebook's internal tests. -## Get involved - -There are many ways to contribute to React Native, and many of them do not involve writing any code. Here's a few ideas to get started: - -* Simply start using React Native. Go through the [Getting Started](https://facebook.github.io/react-native/docs/getting-started.html) guide. Does everything work as expected? If not, we're always looking for improvements. Let us know by [opening an issue](https://facebook.github.io/react-native/docs/contributing.html#reporting-new-issues). -* Look through the [open issues](https://github.com/facebook/react-native/issues). Provide workarounds, ask for clarification, or suggest labels. Help [triage issues](https://facebook.github.io/react-native/docs/contributing.html#triaging-issues-and-pull-requests). -* If you find an issue you would like to fix, [open a pull request](https://facebook.github.io/react-native/docs/contributing.html#your-first-pull-request). Issues tagged as [_Good first issue_](https://github.com/facebook/react-native/labels/Good%20first%20issue) are a good place to get started. -* Read through the [React Native docs](https://facebook.github.io/react-native/docs/getting-started.html). If you find anything that is confusing or can be improved, you can make edits by clicking the "EDIT" button in the top-right corner of most docs. -* Browse [Stack Overflow](https://stackoverflow.com/questions/tagged/react-native) and answer questions. This will help you get familiarized with common pitfalls or misunderstandings, which can be useful when contributing updates to the documentation. -* Take a look at the [features requested](https://react-native.canny.io/feature-requests) by others in the community and consider opening a pull request if you see something you want to work on. - -Contributions are very welcome. If you think you need help planning your contribution, please hop into [#react-native](https://discord.gg/0ZcbPKXt5bZjGY5n) and let people know you're looking for a mentor. - -Core contributors to React Native meet monthly and post their meeting notes on the [React Native blog](https://facebook.github.io/react-native/blog). You can also find ad hoc discussions in the [React Native Core Contributors](https://www.facebook.com/groups/reactnativeoss/) Facebook group. - -### Triaging issues and pull requests - -One great way you can contribute to the project without writing any code is to help triage issues and pull requests as they come in. - -* Ask for more information if the issue does not provide all the details required by the template. -* Suggest [labels](https://github.com/facebook/react-native/labels) that can help categorize issues. -* Flag issues that are stale or that should be closed. -* Ask for test plans and review code. - -You can learn more about handling issues in the [maintainer's guide](docs/maintainers.html#handling-issues). - -## Our development process - -Some of the core team will be working directly on [GitHub](https://github.com/facebook/react-native). These changes will be public from the beginning. Other changesets will come via a bridge with Facebook's internal source control. This is a necessity as it allows engineers at Facebook outside of the core team to move fast and contribute from an environment they are comfortable in. - -When a change made on GitHub is approved, it will first be imported into Facebook's internal source control. The change will eventually sync back to GitHub as a single commit once it has passed all internal tests. - -### Branch organization - -We will do our best to keep `master` in good shape, with tests passing at all times. But in order to move fast, we will make API changes that your application might not be compatible with. We will do our best to [communicate these changes](https://github.com/facebook/react-native/releases) and version appropriately so you can lock into a specific version if need be. - -To see what changes are coming and provide better feedback to React Native contributors, use the [latest release candidate](https://facebook.github.io/react-native/versions.html) when possible. By the time a release candidate is released, the changes it contains will have been shipped in production Facebook apps for over two weeks. - -## Bugs - -We use [GitHub Issues](https://github.com/facebook/react-native/issues) for our public bugs. If you would like to report a problem, take a look around and see if someone already opened an issue about it. If you a are certain this is a new, unreported bug, you can submit a [bug report](https://facebook.github.io/react-native/docs/contributing.html#reporting-new-issues). - -If you have questions about using React Native, the [Community page](https://facebook.github.io/react-native/help.html) list various resources that should help you get started. - -We also have a [place where you can request features or enhancements](https://react-native.canny.io/feature-requests). If you see anything you'd like to be implemented, vote it up and explain your use case. - -## Reporting new issues - -When [opening a new issue](https://github.com/facebook/react-native/issues/new), always make sure to fill out the [issue template](https://raw.githubusercontent.com/facebook/react-native/master/.github/ISSUE_TEMPLATE.md). **This step is very important!** Not doing so may result in your issue getting closed. Don't take this personally if this happens, and feel free to open a new issue once you've gathered all the information required by the template. - -* **One issue, one bug:** Please report a single bug per issue. -* **Provide a Snack:** The best way to get attention on your issue is to provide a reduced test case. You can use [Snack](https://snack.expo.io/) to demonstrate the issue. -* **Provide reproduction steps:** List all the steps necessary to reproduce the issue. Provide a Snack or upload a sample project to GitHub. The person reading your bug report should be able to follow these steps to reproduce your issue with minimal effort. -* **Try out the latest version:** Verify that the issue can be reproduced locally by updating your project to use [React Native from `master`](https://facebook.github.io/react-native/versions.html). The bug may have already been fixed! - -We're not able to provide support through GitHub Issues. If you're looking for help with your code, consider asking on [Stack Overflow](https://stackoverflow.com/questions/tagged/react-native) or reaching out to the community through [other channels](https://facebook.github.io/react-native/support.html). - -### Security bugs - -Facebook has a [bounty program](https://www.facebook.com/whitehat/) for the safe disclosure of security bugs. With that in mind, please do not file public issues; go through the process outlined on that page. - -## Pull requests - -### Your first pull request - -So you have decided to contribute code back to upstream by opening a pull request. You've invested a good chunk of time, and we appreciate it. We will do our best to work with you and get the PR looked at. - -Working on your first Pull Request? You can learn how from this free video series: - -[**How to Contribute to an Open Source Project on GitHub**](https://egghead.io/courses/how-to-contribute-to-an-open-source-project-on-github) - -We have a list of [beginner friendly issues](https://github.com/facebook/react-native/labels/Good%20first%20issue) to help you get your feet wet in the React Native codebase and familiar with our contribution process. This is a great place to get started. - -### Proposing a change - -If you would like to request a new feature or enhancement but are not yet thinking about opening a pull request, we have a [place to track feature requests](https://react-native.canny.io/feature-requests). - -If you intend to change the public API, or make any non-trivial changes to the implementation, we recommend [filing an issue](https://github.com/facebook/react-native/issues/new?title=%5BProposal%5D) that includes `[Proposal]` in the title. This lets us reach an agreement on your proposal before you put significant effort into it. These types of issues should be rare. If you have been contributing to the project long enough, you will probably already have access to the [React Native Core Contributors](https://www.facebook.com/groups/reactnativeoss/) Facebook Group, where this sort of discussion is usually held. - -If you're only fixing a bug, it's fine to submit a pull request right away but we still recommend to file an issue detailing what you're fixing. This is helpful in case we don't accept that specific fix but want to keep track of the issue. - -### Sending a pull request - -Small pull requests are much easier to review and more likely to get merged. Make sure the PR does only one thing, otherwise please split it. +## Pull Requests Please make sure the following is done when submitting a pull request: -1. Fork [the repository](https://github.com/facebook/react-native) and create your branch from `master`. -2. Add the copyright notice to the top of any new files you've added. -3. Describe your [**test plan**](https://facebook.github.io/react-native/docs/contributing.html#test-plan) in your pull request description. Make sure to [test your changes](https://facebook.github.io/react-native/docs/testing.html)! -4. Make sure your code lints (`npm run lint`). -5. If you haven't already, [sign the CLA](https://code.facebook.com/cla). - -All pull requests should be opened against the `master` branch. After opening your pull request, ensure [**all tests pass**](https://facebook.github.io/react-native/docs/contributing.html#contrinuous-integration-tests) on Circle CI. If a test fails and you believe it is unrelated to your change, leave a comment on the pull request explaining why. - -> **Note:** It is not necessary to keep clicking `Merge master to your branch` on the PR page. You would want to merge master if there are conflicts or tests are failing. The Facebook-GitHub-Bot ultimately squashes all commits to a single one before merging your PR. - -#### Test plan - -A good test plan has the exact commands you ran and their output, provides screenshots or videos if the pull request changes UI. - -* If you've added code that should be tested, add tests! -* If you've changed APIs, update the documentation. - -See [What is a Test Plan?](https://medium.com/@martinkonicek/what-is-a-test-plan-8bfc840ec171#.y9lcuqqi9) to learn more. - -#### Continuous integration tests - -Make sure all **tests pass** on [Circle CI][circle]. PRs that break tests are unlikely to be merged. Learn more about [testing your changes here](https://facebook.github.io/react-native/docs/testing.html). - -[circle]: https://circleci.com/gh/facebook/react-native +1. Fork the repo and create your branch from `master`. +2. If you've added code that should be tested, add tests. +3. If you've changed APIs, update the documentation. +4. Ensure the test suite passes. +5. Make sure your code lints. +6. If you haven't already, complete the Contributor License Agreement ("CLA"). -#### Breaking changes +## Contributor License Agreement ("CLA") -When adding a new breaking change, follow this template in your pull request: +In order to accept your pull request, we need you to submit a CLA. You only need to do this once to work on any of Facebook's open source projects. -``` -### New breaking change here +Complete your CLA here: -* **Who does this affect**: -* **How to migrate**: -* **Why make this breaking change**: -* **Severity (number of people affected x effort)**: -``` +## Issues -If your pull request is merged, a core contributor will update the [list of breaking changes](https://github.com/facebook/react-native/wiki/Breaking-Changes) which is then used to populate the release notes. +We use GitHub issues to track public bugs. Please ensure your description is clear and has sufficient instructions to be able to reproduce the issue. -#### Copyright Notice for files +Facebook has a [bounty program](https://www.facebook.com/whitehat/) for the safe disclosure of security bugs. In those cases, please go through the process outlined on that page and do not file a public issue. -Copy and paste this to the top of your new file(s): +## Coding Style -```JS -/** - * Copyright (c) 2015-present, Facebook, Inc. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ -``` +We use Prettier to format our JavaScript code. This saves you time and energy as you can let Prettier fix up any formatting issues automatically through its editor integrations, or by manually running `npm run prettier`. We also use a linter to catch styling issues that may exist in your code. You can check the status of your code styling by simply running `npm run lint`. -#### Contributor License Agreement (CLA) +However, there are still some styles that the linter cannot pick up, notably in Java or Objective-C code. -In order to accept your pull request, we need you to submit a CLA. You only need to do this once, so if you've done this for another Facebook open source project, you're good to go. If you are submitting a pull request for the first time, the Facebook GitHub Bot will reply with a link to the CLA form. You may also [complete your CLA here](https://code.facebook.com/cla). - -### What happens next? - -The core team will be monitoring for pull requests. Read [what to expect from maintainers](https://facebook.github.io/react-native/docs/maintainers.html#handling-pull-requests) to understand what may happen after you open a pull request. - -## Style Guide - -Our linter will catch most styling issues that may exist in your code. You can check the status of your code styling by simply running `npm run lint`. - -However, there are still some styles that the linter cannot pick up. - -### Code Conventions - -#### General - -* **Most important: Look around.** Match the style you see used in the rest of the project. This includes formatting, naming things in code, naming things in documentation. -* Add trailing commas, -* 2 spaces for indentation (no tabs) -* "Attractive" - -#### JavaScript - -* Use semicolons; -* ES6 standards -* Prefer `'` over `"` -* Do not use the optional parameters of `setTimeout` and `setInterval` -* 80 character line length - -#### JSX - -* Prefer `"` over `'` for string literal props -* When wrapping opening tags over multiple lines, place one prop per line -* `{}` of props should hug their values (no spaces) -* Place the closing `>` of opening tags on the same line as the last prop -* Place the closing `/>` of self-closing tags on their own line and left-align them with the opening `<` - -#### Objective-C +**Objective-C:** * Space after `@property` declarations * Brackets on *every* `if`, on the *same* line @@ -194,17 +43,13 @@ However, there are still some styles that the linter cannot pick up. * *Try* to keep it around 80 characters line length (sometimes it's just not possible...) * `*` operator goes with the variable name (e.g. `NSObject *variableName;`) -#### Java +**Java:** * If a method call spans multiple lines closing bracket is on the same line as the last argument. * If a method header doesn't fit on one line each argument goes on a separate line. * 100 character line length -### Documentation - -* Do not wrap lines at 80 characters - configure your editor to soft-wrap when editing documentation. - ## License -By contributing to React Native, you agree that your contributions will be licensed under its MIT license. - +By contributing to React Native, you agree that your contributions will be licensed +under the LICENSE file in the root directory of this source tree. diff --git a/ContainerShip/Dockerfile.android b/ContainerShip/Dockerfile.android index ab311060b2c328..28a923710d6a0d 100644 --- a/ContainerShip/Dockerfile.android +++ b/ContainerShip/Dockerfile.android @@ -1,10 +1,15 @@ +# 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. + # React Native Android Unit Tests # # This image builds upon the React Native Base Android Development Environment # image. Ideally, this image would be rebuilt with any new commit to the master # branch. Doing so will catch issues such as BUCK failing to fetch dependencies # or run tests, as well as unit test failures. -FROM reactnativeci/android-base:latest +FROM react-native-community/react-native LABEL Description="This image prepares and runs React Native's Android tests." LABEL maintainer="HĆ©ctor Ramos " @@ -42,10 +47,10 @@ ADD build.gradle /app/build.gradle ADD react.gradle /app/react.gradle # run gradle downloads -RUN ./gradlew :ReactAndroid:downloadBoost :ReactAndroid:downloadDoubleConversion :ReactAndroid:downloadFolly :ReactAndroid:downloadGlog :ReactAndroid:downloadJSCHeaders +RUN ./gradlew :ReactAndroid:downloadBoost :ReactAndroid:downloadDoubleConversion :ReactAndroid:downloadFolly :ReactAndroid:downloadGlog :ReactAndroid:downloadJSC # compile native libs with Gradle script, we need bridge for unit and integration tests -RUN ./gradlew :ReactAndroid:packageReactNdkLibsForBuck -Pjobs=1 -Pcom.android.build.threadPoolSize=1 +RUN ./gradlew :ReactAndroid:packageReactNdkLibsForBuck -Pjobs=1 # add all react-native code ADD . /app diff --git a/ContainerShip/Dockerfile.android-base b/ContainerShip/Dockerfile.android-base deleted file mode 100644 index 5d19bc7f1e9457..00000000000000 --- a/ContainerShip/Dockerfile.android-base +++ /dev/null @@ -1,78 +0,0 @@ -# React Native Base Android Development Environment -# -# This image provides a base Android development environment for React Native, -# including, but not limited to, the Android SDK, Android NDK, Node, and BUCK. -# These are required in order to run React Native's Android unit and integration -# tests. -# -# This image is not currently built automatically as part of React Native's CI -# infrastructure. It should not be necessary to rebuild this image while the -# Android dependencies (Android SDK version, build tools version, etc) remain -# equal. The operations performed to build this image are those that tend to -# remain stable across commits in any given React Native release. - -FROM library/ubuntu:16.04 - -LABEL Description="This image provides a base Android development environment for React Native, and may be used to run tests." -LABEL maintainer="HĆ©ctor Ramos " - -# set default build arguments -ARG SDK_VERSION=sdk-tools-linux-3859397.zip -ARG ANDROID_BUILD_VERSION=26 -ARG ANDROID_TOOLS_VERSION=26.0.3 -ARG BUCK_VERSION=v2018.07.23.01 -ARG NDK_VERSION=17b -ARG NODE_VERSION=8.10.0 -ARG WATCHMAN_VERSION=4.9.0 - -# set default environment variables -ENV ADB_INSTALL_TIMEOUT=10 -ENV PATH=${PATH}:/opt/buck/bin/ -ENV ANDROID_HOME=/opt/android -ENV ANDROID_SDK_HOME=${ANDROID_HOME} -ENV PATH=${PATH}:${ANDROID_HOME}/tools:${ANDROID_HOME}/tools/bin:${ANDROID_HOME}/platform-tools -ENV ANDROID_NDK=/opt/ndk/android-ndk-r$NDK_VERSION -ENV PATH=${PATH}:${ANDROID_NDK} - -# install system dependencies -RUN apt-get update && apt-get install ant autoconf automake curl g++ gcc git libqt5widgets5 lib32z1 lib32stdc++6 make maven npm openjdk-8* python-dev python3-dev qml-module-qtquick-controls qtdeclarative5-dev unzip -y - -# configure npm -RUN npm config set spin=false && \ - npm config set progress=false - -# install node -RUN npm install n -g -RUN n $NODE_VERSION - -# download buck -RUN git clone https://github.com/facebook/buck.git /opt/buck --branch $BUCK_VERSION --depth=1 -WORKDIR /opt/buck - -# build buck -RUN ant - -# Full reference at https://dl.google.com/android/repository/repository2-1.xml -# download and unpack android -RUN mkdir /opt/android && \ - cd /opt/android && \ - curl --silent https://dl.google.com/android/repository/${SDK_VERSION} > android.zip && \ - unzip android.zip && \ - rm android.zip - -# download and unpack NDK -RUN mkdir /opt/ndk && \ - cd /opt/ndk && \ - curl --silent https://dl.google.com/android/repository/android-ndk-r$NDK_VERSION-linux-x86_64.zip > ndk.zip && \ - unzip ndk.zip && \ - rm ndk.zip - -# Add android SDK tools -RUN sdkmanager "system-images;android-19;google_apis;armeabi-v7a" \ - "platforms;android-$ANDROID_BUILD_VERSION" \ - "build-tools;$ANDROID_TOOLS_VERSION" \ - "add-ons;addon-google_apis-google-23" \ - "extras;android;m2repository" - -# clean up unnecessary directories -RUN rm -rf /opt/android/system-images/android-19/default/x86 diff --git a/ContainerShip/Dockerfile.javascript b/ContainerShip/Dockerfile.javascript deleted file mode 100644 index 7c1e14bb9b845b..00000000000000 --- a/ContainerShip/Dockerfile.javascript +++ /dev/null @@ -1,19 +0,0 @@ -FROM library/node:6.9.2 - -ENV YARN_VERSION=0.27.5 - -# install dependencies -RUN apt-get update && apt-get install ocaml libelf-dev -y -RUN npm install yarn@$YARN_VERSION -g - -# add code -RUN mkdir /app -ADD . /app - -WORKDIR /app -RUN yarn install --ignore-engines - -WORKDIR website -RUN yarn install --ignore-engines --ignore-platform - -WORKDIR /app diff --git a/ContainerShip/scripts/run-android-ci-instrumentation-tests.js b/ContainerShip/scripts/run-android-ci-instrumentation-tests.js index c16c7990ed317d..a788bf19b80a26 100644 --- a/ContainerShip/scripts/run-android-ci-instrumentation-tests.js +++ b/ContainerShip/scripts/run-android-ci-instrumentation-tests.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -38,7 +38,7 @@ const test_opts = { PATH: argv.path || './ReactAndroid/src/androidTest/java/com/facebook/react/tests', RETRIES: parseInt(argv.retries || 2, 10), - TEST_TIMEOUT: parseInt(argv['test-timeout'] || 1000 * 60 * 10), + TEST_TIMEOUT: parseInt(argv['test-timeout'] || 1000 * 60 * 10, 10), OFFSET: argv.offset, COUNT: argv.count, @@ -68,7 +68,6 @@ testClasses = testClasses.map((clazz) => { // only process subset of the tests at corresponding offset and count if args provided if (test_opts.COUNT != null && test_opts.OFFSET != null) { - const testCount = testClasses.length; const start = test_opts.COUNT * test_opts.OFFSET; const end = start + test_opts.COUNT; diff --git a/ContainerShip/scripts/run-android-docker-instrumentation-tests.sh b/ContainerShip/scripts/run-android-docker-instrumentation-tests.sh index ed7183dec9d5fc..2062b547e2bbc4 100644 --- a/ContainerShip/scripts/run-android-docker-instrumentation-tests.sh +++ b/ContainerShip/scripts/run-android-docker-instrumentation-tests.sh @@ -1,4 +1,10 @@ #!/bin/bash +# +# 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. +# # for buck gen mount -o remount,exec /dev/shm @@ -6,7 +12,7 @@ mount -o remount,exec /dev/shm AVD_UUID=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 8 | head -n 1) # create virtual device -echo no | android create avd -n $AVD_UUID -f -t android-19 --abi default/armeabi-v7a +echo no | android create avd -n "$AVD_UUID" -f -t android-19 --abi default/armeabi-v7a # emulator setup emulator64-arm -avd $AVD_UUID -no-skin -no-audio -no-window -no-boot-anim & @@ -25,11 +31,12 @@ watchman shutdown-server # integration tests # build JS bundle for instrumentation tests -node local-cli/cli.js bundle --platform android --dev true --entry-file ReactAndroid/src/androidTest/js/TestBundle.js --bundle-output ReactAndroid/src/androidTest/assets/AndroidTestBundle.js +node cli.js bundle --platform android --dev true --entry-file ReactAndroid/src/androidTest/js/TestBundle.js --bundle-output ReactAndroid/src/androidTest/assets/AndroidTestBundle.js # build test APK +# shellcheck disable=SC1091 source ./scripts/android-setup.sh && NO_BUCKD=1 retry3 buck install ReactAndroid/src/androidTest/buck-runner:instrumentation-tests --config build.threads=1 # run installed apk with tests -node ./ContainerShip/scripts/run-android-ci-instrumentation-tests.js $* +node ./ContainerShip/scripts/run-android-ci-instrumentation-tests.js "$*" exit $? diff --git a/ContainerShip/scripts/run-android-docker-unit-tests.sh b/ContainerShip/scripts/run-android-docker-unit-tests.sh index 5a58bb352e5421..8653ccd9d19555 100644 --- a/ContainerShip/scripts/run-android-docker-unit-tests.sh +++ b/ContainerShip/scripts/run-android-docker-unit-tests.sh @@ -1,4 +1,10 @@ #!/bin/bash +# +# 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. +# # set default environment variables UNIT_TESTS_BUILD_THREADS="${UNIT_TESTS_BUILD_THREADS:-1}" @@ -9,4 +15,4 @@ mount -o remount,exec /dev/shm set -x # run unit tests -buck test ReactAndroid/src/test/... --config build.threads=$UNIT_TESTS_BUILD_THREADS +buck test ReactAndroid/src/test/... --config build.threads="$UNIT_TESTS_BUILD_THREADS" diff --git a/ContainerShip/scripts/run-ci-e2e-tests.sh b/ContainerShip/scripts/run-ci-e2e-tests.sh index de2b97edb5bb7a..a979d3abcce7d0 100755 --- a/ContainerShip/scripts/run-ci-e2e-tests.sh +++ b/ContainerShip/scripts/run-ci-e2e-tests.sh @@ -1,4 +1,10 @@ #!/bin/bash +# +# 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. +# set -ex @@ -12,11 +18,12 @@ RUN_IOS=0 RUN_JS=0 RETRY_COUNT=${RETRY_COUNT:-2} -AVD_UUID=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 8 | head -n 1) - +AVD_UUID=$(< /dev/urandom tr -dc 'a-zA-Z0-9' | fold -w 8 | head -n 1) ANDROID_NPM_DEPS="appium@1.5.1 mocha@2.4.5 wd@0.3.11 colors@1.0.3 pretty-data2@0.40.1" -CLI_PACKAGE=$ROOT/react-native-cli/react-native-cli-*.tgz -PACKAGE=$ROOT/react-native-*.tgz +CLI_PACKAGE="$ROOT/react-native-cli/react-native-cli-*.tgz" +PACKAGE="$ROOT/react-native-*.tgz" +# Version of react-native-dummy to test against +REACT_DUMMY_PLATFORM=react-native-dummy@0.1.0 # solve issue with max user watches limit echo 65536 | tee -a /proc/sys/fs/inotify/max_user_watches @@ -27,7 +34,7 @@ watchman shutdown-server # $2 -- command to run function retry() { local -r -i max_attempts="$1"; shift - local -r cmd="$@" + local -r cmd="$*" local -i attempt_num=1 until $cmd; do @@ -75,7 +82,7 @@ while :; do done function e2e_suite() { - cd $ROOT + cd "$ROOT" if [ $RUN_ANDROID -eq 0 ] && [ $RUN_IOS -eq 0 ] && [ $RUN_JS -eq 0 ]; then echo "No e2e tests specified!" @@ -88,8 +95,8 @@ function e2e_suite() { # To make sure we actually installed the local version # of react-native, we will create a temp file inside the template # and check that it exists after `react-native init - IOS_MARKER=$(mktemp $ROOT/local-cli/templates/HelloWorld/ios/HelloWorld/XXXXXXXX) - ANDROID_MARKER=$(mktemp ${ROOT}/local-cli/templates/HelloWorld/android/XXXXXXXX) + IOS_MARKER="$(mktemp "$ROOT"/template/ios/HelloWorld/XXXXXXXX)" + ANDROID_MARKER="$(mktemp "$ROOT"/template/android/XXXXXXXX)" # install CLI cd react-native-cli @@ -98,8 +105,8 @@ function e2e_suite() { # can skip cli install for non sudo mode if [ $RUN_CLI_INSTALL -ne 0 ]; then - npm install -g $CLI_PACKAGE - if [ $? -ne 0 ]; then + if ! npm install -g "$CLI_PACKAGE" + then echo "Could not install react-native-cli globally, please run in su mode" echo "Or with --skip-cli-install to skip this step" return 1 @@ -111,7 +118,7 @@ function e2e_suite() { # create virtual device if ! android list avd | grep "$AVD_UUID" > /dev/null; then - echo no | android create avd -n $AVD_UUID -f -t android-19 --abi default/armeabi-v7a + echo no | android create avd -n "$AVD_UUID" -f -t android-19 --abi default/armeabi-v7a fi # newline at end of adb devices call and first line is headers @@ -124,9 +131,10 @@ function e2e_suite() { fi # emulator setup - emulator64-arm -avd $AVD_UUID -no-skin -no-audio -no-window -no-boot-anim & + emulator64-arm -avd "$AVD_UUID" -no-skin -no-audio -no-window -no-boot-anim & bootanim="" + # shellcheck disable=SC2076 until [[ "$bootanim" =~ "stopped" ]]; do sleep 5 bootanim=$(adb -e shell getprop init.svc.bootanim 2>&1) @@ -135,23 +143,23 @@ function e2e_suite() { set -ex - ./gradlew :ReactAndroid:installArchives -Pjobs=1 -Dorg.gradle.jvmargs="-Xmx512m -XX:+HeapDumpOnOutOfMemoryError" - if [ $? -ne 0 ]; then + if ! ./gradlew :ReactAndroid:installArchives -Pjobs=1 -Dorg.gradle.jvmargs="-Xmx512m -XX:+HeapDumpOnOutOfMemoryError" + then echo "Failed to compile Android binaries" return 1 fi fi - npm pack - if [ $? -ne 0 ]; then + if ! npm pack + then echo "Failed to pack react-native" return 1 fi - cd $TEMP_DIR + cd "$TEMP_DIR" - retry $RETRY_COUNT react-native init EndToEndTest --version $PACKAGE --npm - if [ $? -ne 0 ]; then + if ! retry "$RETRY_COUNT" react-native init EndToEndTest --version "$PACKAGE" --npm + then echo "Failed to execute react-native init" echo "Most common reason is npm registry connectivity, try again" return 1 @@ -164,20 +172,21 @@ function e2e_suite() { echo "Running an Android e2e test" echo "Installing e2e framework" - retry $RETRY_COUNT npm install --save-dev $ANDROID_NPM_DEPS --silent >> /dev/null - if [ $? -ne 0 ]; then + if ! retry "$RETRY_COUNT" npm install --save-dev "$ANDROID_NPM_DEPS" --silent >> /dev/null + then echo "Failed to install appium" echo "Most common reason is npm registry connectivity, try again" return 1 fi - cp $SCRIPTS/android-e2e-test.js android-e2e-test.js + cp "$SCRIPTS/android-e2e-test.js" android-e2e-test.js - cd android + ( + cd android || exit echo "Downloading Maven deps" ./gradlew :app:copyDownloadableDepsToLibs + ) - cd .. keytool -genkey -v -keystore android/keystores/debug.keystore -storepass android -alias androiddebugkey -keypass android -dname "CN=Android Debug,O=Android,C=US" node ./node_modules/.bin/appium >> /dev/null & @@ -188,9 +197,8 @@ function e2e_suite() { buck build android/app # hack to get node unhung (kill buckd) - kill -9 $(pgrep java) - - if [ $? -ne 0 ]; then + if ! kill -9 "$(pgrep java)" + then echo "could not execute Buck build, is it installed and in PATH?" return 1 fi @@ -201,23 +209,23 @@ function e2e_suite() { sleep 15 echo "Executing android e2e test" - retry $RETRY_COUNT node node_modules/.bin/_mocha android-e2e-test.js - if [ $? -ne 0 ]; then + if ! retry "$RETRY_COUNT" node node_modules/.bin/_mocha android-e2e-test.js + then echo "Failed to run Android e2e tests" echo "Most likely the code is broken" return 1 fi # kill packager process - if kill -0 $SERVER_PID; then + if kill -0 "$SERVER_PID"; then echo "Killing packager $SERVER_PID" - kill -9 $SERVER_PID + kill -9 "$SERVER_PID" fi # kill appium process - if kill -0 $APPIUM_PID; then + if kill -0 "$APPIUM_PID"; then echo "Killing appium $APPIUM_PID" - kill -9 $APPIUM_PID + kill -9 "$APPIUM_PID" fi fi @@ -230,24 +238,37 @@ function e2e_suite() { # js tests if [ $RUN_JS -ne 0 ]; then # Check the packager produces a bundle (doesn't throw an error) - react-native bundle --max-workers 1 --platform android --dev true --entry-file index.js --bundle-output android-bundle.js - if [ $? -ne 0 ]; then + if ! react-native bundle --max-workers 1 --platform android --dev true --entry-file index.js --bundle-output android-bundle.js + then echo "Could not build android bundle" return 1 fi - react-native bundle --max-workers 1 --platform ios --dev true --entry-file index.js --bundle-output ios-bundle.js - if [ $? -ne 0 ]; then + if ! react-native bundle --max-workers 1 --platform ios --dev true --entry-file index.js --bundle-output ios-bundle.js + then echo "Could not build iOS bundle" return 1 fi + + if ! retry "$RETRY_COUNT" npm install --save "$REACT_DUMMY_PLATFORM" --silent >> /dev/null + then + echo "Failed to install react-native-dummy" + echo "Most common reason is npm registry connectivity, try again" + return 1 + fi + + if ! react-native bundle --max-workers 1 --platform dummy --dev true --entry-file index.js --bundle-output dummy-bundle.js + then + echo "Could not build dummy bundle" + return 1 + fi fi # directory cleanup - rm $IOS_MARKER - rm $ANDROID_MARKER + rm "$IOS_MARKER" + rm "$ANDROID_MARKER" return 0 } -retry $RETRY_COUNT e2e_suite +retry "$RETRY_COUNT" e2e_suite diff --git a/ContainerShip/scripts/run-instrumentation-tests-via-adb-shell.sh b/ContainerShip/scripts/run-instrumentation-tests-via-adb-shell.sh index 5a9a976a97da4e..d2b4b9ee275a12 100755 --- a/ContainerShip/scripts/run-instrumentation-tests-via-adb-shell.sh +++ b/ContainerShip/scripts/run-instrumentation-tests-via-adb-shell.sh @@ -1,5 +1,12 @@ #!/bin/bash +# +# 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. +# +# shellcheck disable=SC1117 # Python script to run instrumentation tests, copied from https://github.com/circleci/circle-dummy-android # Example: ./scripts/run-android-instrumentation-tests.sh com.facebook.react.tests com.facebook.react.tests.ReactPickerTestCase # @@ -9,7 +16,7 @@ export PATH="$ANDROID_HOME/platform-tools:$ANDROID_HOME/tools:$PATH" adb logcat -c # run tests and check output -python - $1 $2 << END +python - "$1" "$2" << END import re import subprocess as sp @@ -24,7 +31,7 @@ test_class = None if len(sys.argv) > 2: test_class = sys.argv[2] - + def update(): # prevent CircleCI from killing the process for inactivity while not done: @@ -38,10 +45,10 @@ t.start() def run(): sp.Popen(['adb', 'wait-for-device']).communicate() if (test_class != None): - p = sp.Popen('adb shell am instrument -w -e class %s %s/android.support.test.runner.AndroidJUnitRunner' + p = sp.Popen('adb shell am instrument -w -e class %s %s/android.support.test.runner.AndroidJUnitRunner' % (test_class, test_app), shell=True, stdout=sp.PIPE, stderr=sp.PIPE, stdin=sp.PIPE) else : - p = sp.Popen('adb shell am instrument -w %s/android.support.test.runner.AndroidJUnitRunner' + p = sp.Popen('adb shell am instrument -w %s/android.support.test.runner.AndroidJUnitRunner' % (test_app), shell=True, stdout=sp.PIPE, stderr=sp.PIPE, stdin=sp.PIPE) return p.communicate() diff --git a/DockerTests.md b/DockerTests.md index f1714db87a0a05..682a902e0148d5 100644 --- a/DockerTests.md +++ b/DockerTests.md @@ -1,6 +1,6 @@ # Dockerfile Tests -This is a high level overview of the test configuration using docker. It explains how to run the tests locally +This is a high-level overview of the test configuration using Docker. It explains how to run the tests locally and how they integrate with the Jenkins Pipeline script to run the automated tests on ContainerShip . ## Docker Installation @@ -12,22 +12,22 @@ See for more information on how t We have added a number of default run scripts to the `package.json` file to simplify building and running your tests. -`npm run test-android-setup` - Pulls down the base android docker image used for running the tests +`npm run docker-setup-android` - Pulls down the base android docker image used for running the tests -`npm run test-android-build` - Builds the docker image used to run the tests +`npm run docker-build-android` - Builds the docker image used to run the tests -`npm run test-android-run-unit` - Runs all the unit tests that have been built in the latest react/android docker image (note: you need to run test-android-build before executing this, if the image does not exist it will fail) +`npm run test-android-run-unit` - Runs all the unit tests that have been built in the latest react/android docker image (note: you need to run test-android-build before executing this if the image does not exist it will fail) -`npm run test-android-run-instrumentation` - Runs all the instrumentation tests that have been built in the latest react/android docker image (note: you need to run test-android-build before executing this, if the image does not exist it will fail). You can also pass additional flags to filter which tests instrumentation tests are run. Ex: `npm run test-android-run-instrumentation -- --filter=TestIdTestCase` to only run the TestIdTestCase instrumentation test. See below for more information +`npm run test-android-run-instrumentation` - Runs all the instrumentation tests that have been built in the latest react/android docker image. If the image does not exist, run `test-android-build` before. You can also pass additional flags to filter which tests instrumentation tests are run. Ex: `npm run test-android-run-instrumentation -- --filter=TestIdTestCase` to only run the TestIdTestCase instrumentation test. See below for more information on the instrumentation test flags. -`npm run test-android-run-e2e` - Runs all the end to end tests that have been built in the latest react/android docker image (note: you need to run test-android-build before executing this, if the image does not exist it will fail) +`npm run test-android-run-e2e` - Runs all the end to end tests that have been built in the latest react/android docker image (note: you need to run test-android-build before executing this if the image does not exist it will fail) -`npm run test-android-unit` - Builds and runs the android unit tests. +`npm run test-android-unit` - Builds and runs the Android unit tests. -`npm run test-android-instrumentation` - Builds and runs the android instrumentation tests. +`npm run test-android-instrumentation` - Builds and runs the Android instrumentation tests. -`npm run test-android-e2e` - Builds and runs the android end to end tests. +`npm run test-android-e2e` - Builds and runs the Android end to end tests. ## Detailed Android Setup @@ -35,7 +35,7 @@ There are two Dockerfiles for use with the Android codebase. The `Dockerfile.android-base` contains all the necessary prerequisites required to run the React Android tests. It is separated out into a separate Dockerfile because these are dependencies that rarely change and also because it is quite -a beastly image since it contains all the Android dependencies for running android and the emulators (~9GB). +a beastly image since it contains all the Android dependencies for running Android and the emulators (~9GB). The good news is you should rarely have to build or pull down the base image! All iterative code updates happen as part of the `Dockerfile.android` image build. @@ -62,7 +62,7 @@ The following shell scripts have been provided for android testing: `ContainerShip/scripts/run-android-docker-unit-tests.sh` - Runs the standard android unit tests -`ContainerShip/scripts/run-android-docker-instrumentation-tests.sh` - Runs the android instrumentation tests on the emulator. *Note* that these +`ContainerShip/scripts/run-android-docker-instrumentation-tests.sh` - Runs the Android instrumentation tests on the emulator. *Note* that these tests take quite some time to run so there are various flags you can pass in order to filter which tests are run (see below) `ContainerShip/scripts/run-ci-e2e-tests.sh` - Runs the android end to end tests diff --git a/IntegrationTests/AccessibilityManagerTest.js b/IntegrationTests/AccessibilityManagerTest.js index 41710be91e14c0..0a059d99c27c24 100644 --- a/IntegrationTests/AccessibilityManagerTest.js +++ b/IntegrationTests/AccessibilityManagerTest.js @@ -1,11 +1,11 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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 + * @flow strict-local */ 'use strict'; diff --git a/IntegrationTests/AppEventsTest.js b/IntegrationTests/AppEventsTest.js index 90da0bd87c1ac2..e88e97909d35e9 100644 --- a/IntegrationTests/AppEventsTest.js +++ b/IntegrationTests/AppEventsTest.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/IntegrationTests/AsyncStorageTest.js b/IntegrationTests/AsyncStorageTest.js index 150aa2aed511cf..bcd0abe07bbaea 100644 --- a/IntegrationTests/AsyncStorageTest.js +++ b/IntegrationTests/AsyncStorageTest.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -12,7 +12,7 @@ const React = require('react'); const ReactNative = require('react-native'); -const {AsyncStorage, Text, View} = ReactNative; +const {AsyncStorage, Text, View, StyleSheet} = ReactNative; const {TestModule} = ReactNative.NativeModules; const deepDiffer = require('deepDiffer'); @@ -191,7 +191,7 @@ class AsyncStorageTest extends React.Component<{}, $FlowFixMeState> { render() { return ( - + {/* $FlowFixMe(>=0.54.0 site=react_native_fb,react_native_oss) This * comment suppresses an error found when Flow v0.54 was deployed. @@ -205,6 +205,13 @@ class AsyncStorageTest extends React.Component<{}, $FlowFixMeState> { } } +const styles = StyleSheet.create({ + container: { + backgroundColor: 'white', + padding: 40, + }, +}); + AsyncStorageTest.displayName = 'AsyncStorageTest'; module.exports = AsyncStorageTest; diff --git a/IntegrationTests/ImageCachePolicyTest.js b/IntegrationTests/ImageCachePolicyTest.js index 1dc5d5fb61a9b5..0dae9812cf6e24 100644 --- a/IntegrationTests/ImageCachePolicyTest.js +++ b/IntegrationTests/ImageCachePolicyTest.js @@ -1,11 +1,11 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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 + * @flow strict-local */ 'use strict'; @@ -58,7 +58,7 @@ class ImageCachePolicyTest extends React.Component { render() { return ( - + Hello { } const styles = StyleSheet.create({ + container: { + flex: 1, + }, base: { width: 100, height: 100, diff --git a/IntegrationTests/ImageSnapshotTest.js b/IntegrationTests/ImageSnapshotTest.js index c20406c641ecb5..c4ffaae51c2ec4 100644 --- a/IntegrationTests/ImageSnapshotTest.js +++ b/IntegrationTests/ImageSnapshotTest.js @@ -1,18 +1,18 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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 + * @flow strict-local */ 'use strict'; const React = require('react'); const ReactNative = require('react-native'); -const {Image, View} = ReactNative; +const {Image} = ReactNative; const {TestModule} = ReactNative.NativeModules; class ImageSnapshotTest extends React.Component<{}> { diff --git a/IntegrationTests/IntegrationTestHarnessTest.js b/IntegrationTests/IntegrationTestHarnessTest.js index 045d2e7e1eae1e..efe6cd4cd2f643 100644 --- a/IntegrationTests/IntegrationTestHarnessTest.js +++ b/IntegrationTests/IntegrationTestHarnessTest.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -10,28 +10,22 @@ 'use strict'; -/* $FlowFixMe(>=0.54.0 site=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. */ const requestAnimationFrame = require('fbjs/lib/requestAnimationFrame'); const React = require('react'); -const PropTypes = require('prop-types'); const ReactNative = require('react-native'); -const {Text, View} = ReactNative; +const {Text, View, StyleSheet} = ReactNative; const {TestModule} = ReactNative.NativeModules; -class IntegrationTestHarnessTest extends React.Component< - { - shouldThrow?: boolean, - waitOneFrame?: boolean, - }, - $FlowFixMeState, -> { - static propTypes = { - shouldThrow: PropTypes.bool, - waitOneFrame: PropTypes.bool, - }; +type Props = $ReadOnly<{| + shouldThrow?: boolean, + waitOneFrame?: boolean, +|}>; + +type State = {| + done: boolean, +|}; +class IntegrationTestHarnessTest extends React.Component { state = { done: false, }; @@ -60,7 +54,7 @@ class IntegrationTestHarnessTest extends React.Component< render() { return ( - + {/* $FlowFixMe(>=0.54.0 site=react_native_fb,react_native_oss) This * comment suppresses an error found when Flow v0.54 was deployed. @@ -73,6 +67,13 @@ class IntegrationTestHarnessTest extends React.Component< } } +const styles = StyleSheet.create({ + container: { + backgroundColor: 'white', + padding: 40, + }, +}); + IntegrationTestHarnessTest.displayName = 'IntegrationTestHarnessTest'; module.exports = IntegrationTestHarnessTest; diff --git a/IntegrationTests/IntegrationTestsApp.js b/IntegrationTests/IntegrationTestsApp.js index 421b4f6abe2b19..de88f1d6056f17 100644 --- a/IntegrationTests/IntegrationTestsApp.js +++ b/IntegrationTests/IntegrationTestsApp.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -10,6 +10,7 @@ 'use strict'; +require('InitializeCore'); const React = require('react'); const ReactNative = require('react-native'); const { diff --git a/IntegrationTests/LayoutEventsTest.js b/IntegrationTests/LayoutEventsTest.js index 5d2676bc1550b3..b51544b94e7f29 100644 --- a/IntegrationTests/LayoutEventsTest.js +++ b/IntegrationTests/LayoutEventsTest.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -11,11 +11,12 @@ 'use strict'; const React = require('react'); -const createReactClass = require('create-react-class'); const ReactNative = require('react-native'); const {Image, LayoutAnimation, StyleSheet, Text, View} = ReactNative; const {TestModule} = ReactNative.NativeModules; +import type {ViewStyleProp} from 'StyleSheet'; + const deepDiffer = require('deepDiffer'); function debug(...args) { @@ -23,14 +24,8 @@ function debug(...args) { } import type {Layout, LayoutEvent} from 'CoreEventTypes'; -type Style = { - margin?: number, - padding?: number, - borderColor?: string, - borderWidth?: number, - backgroundColor?: string, - width?: number, -}; + +type Props = $ReadOnly<{||}>; type State = { didAnimation: boolean, @@ -38,44 +33,58 @@ type State = { imageLayout?: Layout, textLayout?: Layout, viewLayout?: Layout, - viewStyle?: Style, - containerStyle?: Style, + viewStyle?: ViewStyleProp, + containerStyle?: ViewStyleProp, }; -const LayoutEventsTest = createReactClass({ - displayName: 'LayoutEventsTest', - getInitialState(): State { - return { - didAnimation: false, - }; - }, - animateViewLayout: function() { +class LayoutEventsTest extends React.Component { + _view: ?React.ElementRef; + _img: ?React.ElementRef; + _txt: ?React.ElementRef; + + state: State = { + didAnimation: false, + }; + + animateViewLayout() { debug('animateViewLayout invoked'); LayoutAnimation.configureNext(LayoutAnimation.Presets.spring, () => { debug('animateViewLayout done'); this.checkLayout(this.addWrapText); }); this.setState({viewStyle: {margin: 60}}); - }, - addWrapText: function() { + } + + addWrapText = () => { debug('addWrapText invoked'); this.setState( {extraText: ' And a bunch more text to wrap around a few lines.'}, () => this.checkLayout(this.changeContainer), ); - }, - changeContainer: function() { + }; + + changeContainer = () => { debug('changeContainer invoked'); this.setState({containerStyle: {width: 280}}, () => this.checkLayout(TestModule.markTestCompleted), ); - }, - checkLayout: function(next?: ?Function) { - if (!this.isMounted()) { + }; + + checkLayout = (next?: ?() => void) => { + const view = this._view; + const txt = this._txt; + const img = this._img; + + if (view == null || txt == null || img == null) { return; } - this.refs.view.measure((x, y, width, height) => { - this.compare('view', {x, y, width, height}, this.state.viewLayout); + + view.measure((x, y, width, height) => { + this.compare( + 'view', + {x, y, width, height}, + this.state.viewLayout || null, + ); if (typeof next === 'function') { next(); } else if (!this.state.didAnimation) { @@ -84,14 +93,17 @@ const LayoutEventsTest = createReactClass({ this.state.didAnimation = true; } }); - this.refs.txt.measure((x, y, width, height) => { + + txt.measure((x, y, width, height) => { this.compare('txt', {x, y, width, height}, this.state.textLayout); }); - this.refs.img.measure((x, y, width, height) => { + + img.measure((x, y, width, height) => { this.compare('img', {x, y, width, height}, this.state.imageLayout); }); - }, - compare: function(node: string, measured: any, onLayout: any): void { + }; + + compare(node: string, measured: Layout, onLayout?: ?Layout): void { if (deepDiffer(measured, onLayout)) { const data = {measured, onLayout}; throw new Error( @@ -100,34 +112,50 @@ const LayoutEventsTest = createReactClass({ JSON.stringify(data, null, ' '), ); } - }, - onViewLayout: function(e: LayoutEvent) { + } + + onViewLayout = (e: LayoutEvent) => { debug('received view layout event\n', e.nativeEvent); this.setState({viewLayout: e.nativeEvent.layout}, this.checkLayout); - }, - onTextLayout: function(e: LayoutEvent) { + }; + + onTextLayout = (e: LayoutEvent) => { debug('received text layout event\n', e.nativeEvent); this.setState({textLayout: e.nativeEvent.layout}, this.checkLayout); - }, - onImageLayout: function(e: LayoutEvent) { + }; + + onImageLayout = (e: LayoutEvent) => { debug('received image layout event\n', e.nativeEvent); this.setState({imageLayout: e.nativeEvent.layout}, this.checkLayout); - }, - render: function() { + }; + + render() { const viewStyle = [styles.view, this.state.viewStyle]; const textLayout = this.state.textLayout || {width: '?', height: '?'}; const imageLayout = this.state.imageLayout || {x: '?', y: '?'}; debug('viewLayout', this.state.viewLayout); return ( - + { + this._view = ref; + }} + onLayout={this.onViewLayout} + style={viewStyle}> { + this._img = ref; + }} onLayout={this.onImageLayout} style={styles.image} source={{uri: 'uie_thumb_big.png'}} /> - + { + this._txt = ref; + }} + onLayout={this.onTextLayout} + style={styles.text}> A simple piece of text.{this.state.extraText} @@ -138,8 +166,8 @@ const LayoutEventsTest = createReactClass({ ); - }, -}); + } +} const styles = StyleSheet.create({ container: { @@ -165,6 +193,4 @@ const styles = StyleSheet.create({ }, }); -LayoutEventsTest.displayName = 'LayoutEventsTest'; - module.exports = LayoutEventsTest; diff --git a/IntegrationTests/LoggingTestModule.js b/IntegrationTests/LoggingTestModule.js index 905abee400828d..ba3475b8d94bda 100644 --- a/IntegrationTests/LoggingTestModule.js +++ b/IntegrationTests/LoggingTestModule.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -12,7 +12,7 @@ const BatchedBridge = require('BatchedBridge'); const warning = require('fbjs/lib/warning'); -const invariant = require('fbjs/lib/invariant'); +const invariant = require('invariant'); const LoggingTestModule = { logToConsole: function(str) { diff --git a/IntegrationTests/PromiseTest.js b/IntegrationTests/PromiseTest.js index facf8b45cdc746..c36d3beb017b69 100644 --- a/IntegrationTests/PromiseTest.js +++ b/IntegrationTests/PromiseTest.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/IntegrationTests/PropertiesUpdateTest.js b/IntegrationTests/PropertiesUpdateTest.js index 33d17da5e5c442..8c61685bf8848e 100644 --- a/IntegrationTests/PropertiesUpdateTest.js +++ b/IntegrationTests/PropertiesUpdateTest.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/IntegrationTests/RCTRootViewIntegrationTestApp.js b/IntegrationTests/RCTRootViewIntegrationTestApp.js index 6afda59cd688c1..2466b6378586eb 100644 --- a/IntegrationTests/RCTRootViewIntegrationTestApp.js +++ b/IntegrationTests/RCTRootViewIntegrationTestApp.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -9,8 +9,6 @@ 'use strict'; -require('regenerator-runtime/runtime'); - const React = require('react'); const ReactNative = require('react-native'); diff --git a/IntegrationTests/ReactContentSizeUpdateTest.js b/IntegrationTests/ReactContentSizeUpdateTest.js index be0b06b6abf56b..707abc6cb727e3 100644 --- a/IntegrationTests/ReactContentSizeUpdateTest.js +++ b/IntegrationTests/ReactContentSizeUpdateTest.js @@ -1,78 +1,89 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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 React = require('react'); -const createReactClass = require('create-react-class'); const ReactNative = require('react-native'); const RCTNativeAppEventEmitter = require('RCTNativeAppEventEmitter'); -const Subscribable = require('Subscribable'); -const TimerMixin = require('react-timer-mixin'); const {View} = ReactNative; const {TestModule} = ReactNative.NativeModules; +import type EmitterSubscription from 'EmitterSubscription'; const reactViewWidth = 101; const reactViewHeight = 102; const newReactViewWidth = 201; const newReactViewHeight = 202; -const ReactContentSizeUpdateTest = createReactClass({ - displayName: 'ReactContentSizeUpdateTest', - mixins: [Subscribable.Mixin, TimerMixin], +type Props = {||}; - UNSAFE_componentWillMount: function() { - this.addListenerOn( - RCTNativeAppEventEmitter, +type State = {| + height: number, + width: number, +|}; + +class ReactContentSizeUpdateTest extends React.Component { + _timeoutID: ?TimeoutID = null; + _subscription: ?EmitterSubscription = null; + + state = { + height: reactViewHeight, + width: reactViewWidth, + }; + + UNSAFE_componentWillMount() { + this._subscription = RCTNativeAppEventEmitter.addListener( 'rootViewDidChangeIntrinsicSize', this.rootViewDidChangeIntrinsicSize, ); - }, + } - getInitialState: function() { - return { - height: reactViewHeight, - width: reactViewWidth, - }; - }, + componentDidMount() { + this._timeoutID = setTimeout(() => { + this.updateViewSize(); + }, 1000); + } + + componentWillUnmount() { + if (this._timeoutID != null) { + clearTimeout(this._timeoutID); + } - updateViewSize: function() { + if (this._subscription != null) { + this._subscription.remove(); + } + } + + updateViewSize() { this.setState({ height: newReactViewHeight, width: newReactViewWidth, }); - }, - - componentDidMount: function() { - this.setTimeout(() => { - this.updateViewSize(); - }, 1000); - }, + } - rootViewDidChangeIntrinsicSize: function(intrinsicSize) { + rootViewDidChangeIntrinsicSize = (intrinsicSize: State) => { if ( intrinsicSize.height === newReactViewHeight && intrinsicSize.width === newReactViewWidth ) { TestModule.markTestPassed(true); } - }, + }; render() { return ( ); - }, -}); - -ReactContentSizeUpdateTest.displayName = 'ReactContentSizeUpdateTest'; + } +} module.exports = ReactContentSizeUpdateTest; diff --git a/IntegrationTests/SimpleSnapshotTest.js b/IntegrationTests/SimpleSnapshotTest.js index 2857f3f41b492a..fef8377373d0f6 100644 --- a/IntegrationTests/SimpleSnapshotTest.js +++ b/IntegrationTests/SimpleSnapshotTest.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -34,7 +34,7 @@ class SimpleSnapshotTest extends React.Component<{}> { render() { return ( - + @@ -43,6 +43,10 @@ class SimpleSnapshotTest extends React.Component<{}> { } const styles = StyleSheet.create({ + container: { + backgroundColor: 'white', + padding: 100, + }, box1: { width: 80, height: 50, diff --git a/IntegrationTests/SizeFlexibilityUpdateTest.js b/IntegrationTests/SizeFlexibilityUpdateTest.js index 0d0ebd4bfc20f3..42ce5bf5fb8173 100644 --- a/IntegrationTests/SizeFlexibilityUpdateTest.js +++ b/IntegrationTests/SizeFlexibilityUpdateTest.js @@ -1,46 +1,60 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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 React = require('react'); -const createReactClass = require('create-react-class'); const ReactNative = require('react-native'); const RCTNativeAppEventEmitter = require('RCTNativeAppEventEmitter'); -const Subscribable = require('Subscribable'); const {View} = ReactNative; const {TestModule} = ReactNative.NativeModules; +import type EmitterSubscription from 'EmitterSubscription'; const reactViewWidth = 111; const reactViewHeight = 222; let finalState = false; -const SizeFlexibilityUpdateTest = createReactClass({ - displayName: 'SizeFlexibilityUpdateTest', - mixins: [Subscribable.Mixin], +type Props = $ReadOnly<{| + width: boolean, + height: boolean, + both: boolean, + none: boolean, +|}>; - UNSAFE_componentWillMount: function() { - this.addListenerOn( - RCTNativeAppEventEmitter, +class SizeFlexibilityUpdateTest extends React.Component { + _subscription: ?EmitterSubscription = null; + + UNSAFE_componentWillMount() { + this._subscription = RCTNativeAppEventEmitter.addListener( 'rootViewDidChangeIntrinsicSize', this.rootViewDidChangeIntrinsicSize, ); - }, + } + + componentWillUnmount() { + if (this._subscription != null) { + this._subscription.remove(); + } + } - markPassed: function() { + markPassed = () => { TestModule.markTestPassed(true); finalState = true; - }, + }; - rootViewDidChangeIntrinsicSize: function(intrinsicSize) { + rootViewDidChangeIntrinsicSize = (intrinsicSize: { + width: number, + height: number, + }) => { if (finalState) { // If a test reaches its final state, it is not expected to do anything more TestModule.markTestPassed(false); @@ -83,13 +97,11 @@ const SizeFlexibilityUpdateTest = createReactClass({ return; } } - }, + }; render() { return ; - }, -}); - -SizeFlexibilityUpdateTest.displayName = 'SizeFlexibilityUpdateTest'; + } +} module.exports = SizeFlexibilityUpdateTest; diff --git a/IntegrationTests/SyncMethodTest.js b/IntegrationTests/SyncMethodTest.js index 6efef977d81194..d618ced4761585 100644 --- a/IntegrationTests/SyncMethodTest.js +++ b/IntegrationTests/SyncMethodTest.js @@ -1,11 +1,11 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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 + * @flow strict-local */ 'use strict'; diff --git a/IntegrationTests/TimersTest.js b/IntegrationTests/TimersTest.js index 15d42c4c852a1c..c7eefb915333c3 100644 --- a/IntegrationTests/TimersTest.js +++ b/IntegrationTests/TimersTest.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -11,75 +11,136 @@ 'use strict'; const React = require('react'); -const createReactClass = require('create-react-class'); const ReactNative = require('react-native'); -/* $FlowFixMe(>=0.54.0 site=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. */ -const TimerMixin = require('react-timer-mixin'); - const {StyleSheet, Text, View} = ReactNative; const {TestModule} = ReactNative.NativeModules; -const TimersTest = createReactClass({ - displayName: 'TimersTest', - mixins: [TimerMixin], +type Props = $ReadOnly<{||}>; - _nextTest: () => {}, - _interval: -1, +type State = {| + count: number, + done: boolean, +|}; - getInitialState() { - return { - count: 0, - done: false, - }; - }, +type ImmediateID = Object; + +class TimersTest extends React.Component { + _nextTest = () => {}; + _interval: ?IntervalID = null; + + _timeoutIDs: Set = new Set(); + _intervalIDs: Set = new Set(); + _immediateIDs: Set = new Set(); + _animationFrameIDs: Set = new Set(); + + state = { + count: 0, + done: false, + }; + + setTimeout(fn: () => void, time: number): TimeoutID { + const id = setTimeout(() => { + this._timeoutIDs.delete(id); + fn(); + }, time); + + this._timeoutIDs.add(id); + + return id; + } + + clearTimeout(id: TimeoutID) { + this._timeoutIDs.delete(id); + clearTimeout(id); + } + + setInterval(fn: () => void, time: number): IntervalID { + const id = setInterval(() => { + fn(); + }, time); + + this._intervalIDs.add(id); + + return id; + } + + clearInterval(id: IntervalID) { + this._intervalIDs.delete(id); + clearInterval(id); + } + + setImmediate(fn: () => void): ImmediateID { + const id = setImmediate(() => { + this._immediateIDs.delete(id); + fn(); + }); + + this._immediateIDs.add(id); + + return id; + } + + requestAnimationFrame(fn: () => void): AnimationFrameID { + const id = requestAnimationFrame(() => { + this._animationFrameIDs.delete(id); + fn(); + }); + + this._animationFrameIDs.add(id); + + return id; + } + + cancelAnimationFrame(id: AnimationFrameID): void { + this._animationFrameIDs.delete(id); + cancelAnimationFrame(id); + } componentDidMount() { this.setTimeout(this.testSetTimeout0, 1000); - }, + } testSetTimeout0() { this.setTimeout(this.testSetTimeout1, 0); - }, + } testSetTimeout1() { this.setTimeout(this.testSetTimeout50, 1); - }, + } testSetTimeout50() { this.setTimeout(this.testRequestAnimationFrame, 50); - }, + } testRequestAnimationFrame() { this.requestAnimationFrame(this.testSetInterval0); - }, + } testSetInterval0() { this._nextTest = this.testSetInterval20; this._interval = this.setInterval(this._incrementInterval, 0); - }, + } testSetInterval20() { this._nextTest = this.testSetImmediate; this._interval = this.setInterval(this._incrementInterval, 20); - }, + } testSetImmediate() { this.setImmediate(this.testClearTimeout0); - }, + } testClearTimeout0() { const timeout = this.setTimeout(() => this._fail('testClearTimeout0'), 0); this.clearTimeout(timeout); this.testClearTimeout30(); - }, + } testClearTimeout30() { const timeout = this.setTimeout(() => this._fail('testClearTimeout30'), 30); this.clearTimeout(timeout); this.setTimeout(this.testClearMulti, 50); - }, + } testClearMulti() { const fails = []; @@ -96,7 +157,7 @@ const TimersTest = createReactClass({ this.setTimeout(() => this.clearTimeout(delayClear), 20); this.setTimeout(this.testOrdering, 50); - }, + } testOrdering() { // Clear timers are set first because it's more likely to uncover bugs. @@ -131,42 +192,73 @@ const TimersTest = createReactClass({ 25, ); this.setTimeout(this.done, 50); - }, + } done() { this.setState({done: true}, () => { TestModule.markTestCompleted(); }); - }, + } + + componentWillUnmount() { + for (const timeoutID of this._timeoutIDs) { + clearTimeout(timeoutID); + } + + for (const intervalID of this._intervalIDs) { + clearInterval(intervalID); + } + + for (const requestAnimationFrameID of this._animationFrameIDs) { + cancelAnimationFrame(requestAnimationFrameID); + } + + for (const immediateID of this._immediateIDs) { + clearImmediate(immediateID); + } + + this._timeoutIDs = new Set(); + this._intervalIDs = new Set(); + this._animationFrameIDs = new Set(); + this._immediateIDs = new Set(); + + if (this._interval != null) { + clearInterval(this._interval); + this._interval = null; + } + } render() { return ( - {this.constructor.displayName + ': \n'} + {this.constructor.name + ': \n'} Intervals: {this.state.count + '\n'} {this.state.done ? 'Done' : 'Testing...'} ); - }, + } _incrementInterval() { if (this.state.count > 3) { throw new Error('interval incremented past end.'); } if (this.state.count === 3) { - this.clearInterval(this._interval); + if (this._interval != null) { + this.clearInterval(this._interval); + this._interval = null; + } this.setState({count: 0}, this._nextTest); return; } this.setState({count: this.state.count + 1}); - }, + } _fail(caller: string): void { throw new Error('_fail called by ' + caller); - }, -}); + } +} const styles = StyleSheet.create({ container: { @@ -175,6 +267,4 @@ const styles = StyleSheet.create({ }, }); -TimersTest.displayName = 'TimersTest'; - module.exports = TimersTest; diff --git a/IntegrationTests/WebSocketTest.js b/IntegrationTests/WebSocketTest.js index f2478cbcb68539..0d2cd5e0b4d9e7 100644 --- a/IntegrationTests/WebSocketTest.js +++ b/IntegrationTests/WebSocketTest.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -18,12 +18,6 @@ const {TestModule} = ReactNative.NativeModules; const DEFAULT_WS_URL = 'ws://localhost:5555/'; const WS_EVENTS = ['close', 'error', 'message', 'open']; -const WS_STATES = [ - /* 0 */ 'CONNECTING', - /* 1 */ 'OPEN', - /* 2 */ 'CLOSING', - /* 3 */ 'CLOSED', -]; type State = { url: string, @@ -50,7 +44,6 @@ class WebSocketTest extends React.Component<{}, State> { _waitFor = (condition: any, timeout: any, callback: any) => { let remaining = timeout; - let t; const timeoutFunction = function() { if (condition()) { callback(true); @@ -60,10 +53,10 @@ class WebSocketTest extends React.Component<{}, State> { if (remaining === 0) { callback(false); } else { - t = setTimeout(timeoutFunction, 1000); + setTimeout(timeoutFunction, 1000); } }; - t = setTimeout(timeoutFunction, 1000); + setTimeout(timeoutFunction, 1000); }; _connect = () => { @@ -121,39 +114,30 @@ class WebSocketTest extends React.Component<{}, State> { } testConnect = () => { - const component = this; - component._connect(); - component._waitFor(component._socketIsConnected, 5, function( - connectSucceeded, - ) { + this._connect(); + this._waitFor(this._socketIsConnected, 5, connectSucceeded => { if (!connectSucceeded) { TestModule.markTestPassed(false); return; } - component.testSendAndReceive(); + this.testSendAndReceive(); }); }; testSendAndReceive = () => { - const component = this; - component._sendTestMessage(); - component._waitFor(component._receivedTestExpectedResponse, 5, function( - messageReceived, - ) { + this._sendTestMessage(); + this._waitFor(this._receivedTestExpectedResponse, 5, messageReceived => { if (!messageReceived) { TestModule.markTestPassed(false); return; } - component.testDisconnect(); + this.testDisconnect(); }); }; testDisconnect = () => { - const component = this; - component._disconnect(); - component._waitFor(component._socketIsDisconnected, 5, function( - disconnectSucceeded, - ) { + this._disconnect(); + this._waitFor(this._socketIsDisconnected, 5, disconnectSucceeded => { TestModule.markTestPassed(disconnectSucceeded); }); }; diff --git a/IntegrationTests/WebViewTest.js b/IntegrationTests/WebViewTest.js index 74dbeddec6b5ce..269b9d24bdee81 100644 --- a/IntegrationTests/WebViewTest.js +++ b/IntegrationTests/WebViewTest.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/IntegrationTests/launchWebSocketServer.command b/IntegrationTests/launchWebSocketServer.command index 445f3fc0fa8857..1ce2cf5317c010 100755 --- a/IntegrationTests/launchWebSocketServer.command +++ b/IntegrationTests/launchWebSocketServer.command @@ -1,6 +1,6 @@ #!/bin/bash -# Copyright (c) 2015-present, Facebook, Inc. +# 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. diff --git a/IntegrationTests/websocket_integration_test_server.js b/IntegrationTests/websocket_integration_test_server.js index 50a6568710a2b8..67b358d70b09dd 100755 --- a/IntegrationTests/websocket_integration_test_server.js +++ b/IntegrationTests/websocket_integration_test_server.js @@ -1,7 +1,7 @@ #!/usr/bin/env node /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/LICENSE b/LICENSE index 9e051010d82dc6..b96dcb0480a0b0 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2015-present, Facebook, Inc. +Copyright (c) Facebook, Inc. and its affiliates. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Libraries/ART/ARTCGFloatArray.h b/Libraries/ART/ARTCGFloatArray.h index 72286a50527e84..b26bcb1b7dae96 100644 --- a/Libraries/ART/ARTCGFloatArray.h +++ b/Libraries/ART/ARTCGFloatArray.h @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -7,7 +7,7 @@ // A little helper to make sure we have the right memory allocation ready for use. // We assume that we will only this in one place so no reference counting is necessary. -// Needs to be freed when dealloced. +// Needs to be freed when deallocated. // This is fragile since this relies on these values not getting reused. Consider // wrapping these in an Obj-C class or some ARC hackery to get refcounting. diff --git a/Libraries/ART/ARTContainer.h b/Libraries/ART/ARTContainer.h index 532145825bd74b..5439d9fb1d2176 100644 --- a/Libraries/ART/ARTContainer.h +++ b/Libraries/ART/ARTContainer.h @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/ART/ARTGroup.h b/Libraries/ART/ARTGroup.h index d9dcb48c87b180..31edddde7b664f 100644 --- a/Libraries/ART/ARTGroup.h +++ b/Libraries/ART/ARTGroup.h @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/ART/ARTGroup.m b/Libraries/ART/ARTGroup.m index 1bc70725c030e8..56699114f4a3f2 100644 --- a/Libraries/ART/ARTGroup.m +++ b/Libraries/ART/ARTGroup.m @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/ART/ARTNode.h b/Libraries/ART/ARTNode.h index 8b66c205ef2440..9f381115a7646e 100644 --- a/Libraries/ART/ARTNode.h +++ b/Libraries/ART/ARTNode.h @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/ART/ARTNode.m b/Libraries/ART/ARTNode.m index b27b014b9e7d0e..ae1a152a7d7626 100644 --- a/Libraries/ART/ARTNode.m +++ b/Libraries/ART/ARTNode.m @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/ART/ARTRenderable.h b/Libraries/ART/ARTRenderable.h index e5735a1fbd81ef..077d47ec2c6c32 100644 --- a/Libraries/ART/ARTRenderable.h +++ b/Libraries/ART/ARTRenderable.h @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/ART/ARTRenderable.m b/Libraries/ART/ARTRenderable.m index d7a3115586f3dd..88349f8296e7b0 100644 --- a/Libraries/ART/ARTRenderable.m +++ b/Libraries/ART/ARTRenderable.m @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/ART/ARTSerializablePath.js b/Libraries/ART/ARTSerializablePath.js index 9c2d98e033fcc3..aafb74c45d065c 100644 --- a/Libraries/ART/ARTSerializablePath.js +++ b/Libraries/ART/ARTSerializablePath.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/ART/ARTShape.h b/Libraries/ART/ARTShape.h index ce685c564b06be..1a1fd429a8bf1a 100644 --- a/Libraries/ART/ARTShape.h +++ b/Libraries/ART/ARTShape.h @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/ART/ARTShape.m b/Libraries/ART/ARTShape.m index 935c8894994a2b..8a1a7d6979f3e9 100644 --- a/Libraries/ART/ARTShape.m +++ b/Libraries/ART/ARTShape.m @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/ART/ARTSurfaceView.h b/Libraries/ART/ARTSurfaceView.h index cb42928cedbe12..34016b9ea7f8ef 100644 --- a/Libraries/ART/ARTSurfaceView.h +++ b/Libraries/ART/ARTSurfaceView.h @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/ART/ARTSurfaceView.m b/Libraries/ART/ARTSurfaceView.m index c8f46069038603..7e033690d1dc99 100644 --- a/Libraries/ART/ARTSurfaceView.m +++ b/Libraries/ART/ARTSurfaceView.m @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/ART/ARTText.h b/Libraries/ART/ARTText.h index cdf8393a137629..1431fc5e181c5e 100644 --- a/Libraries/ART/ARTText.h +++ b/Libraries/ART/ARTText.h @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/ART/ARTText.m b/Libraries/ART/ARTText.m index 0953c850dc293e..5bcebdf5559942 100644 --- a/Libraries/ART/ARTText.m +++ b/Libraries/ART/ARTText.m @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/ART/ARTTextFrame.h b/Libraries/ART/ARTTextFrame.h index 8ad06e0a01baed..00e61ce263d76f 100644 --- a/Libraries/ART/ARTTextFrame.h +++ b/Libraries/ART/ARTTextFrame.h @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -9,7 +9,7 @@ // A little helper to make sure we have a set of lines including width ready for use. // We assume that we will only this in one place so no reference counting is necessary. -// Needs to be freed when dealloced. +// Needs to be freed when deallocated. // This is fragile since this relies on these values not getting reused. Consider // wrapping these in an Obj-C class or some ARC hackery to get refcounting. diff --git a/Libraries/ART/Brushes/ARTBrush.h b/Libraries/ART/Brushes/ARTBrush.h index 95709e65f19bc9..f6e39e861f737c 100644 --- a/Libraries/ART/Brushes/ARTBrush.h +++ b/Libraries/ART/Brushes/ARTBrush.h @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/ART/Brushes/ARTBrush.m b/Libraries/ART/Brushes/ARTBrush.m index 4ade2c221143f7..969ad5d7edb363 100644 --- a/Libraries/ART/Brushes/ARTBrush.m +++ b/Libraries/ART/Brushes/ARTBrush.m @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/ART/Brushes/ARTLinearGradient.h b/Libraries/ART/Brushes/ARTLinearGradient.h index 8e6abbc5fbbfde..d289b192a51b1e 100644 --- a/Libraries/ART/Brushes/ARTLinearGradient.h +++ b/Libraries/ART/Brushes/ARTLinearGradient.h @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/ART/Brushes/ARTLinearGradient.m b/Libraries/ART/Brushes/ARTLinearGradient.m index 1415a6ffcbc80f..20bd1af6233b4e 100644 --- a/Libraries/ART/Brushes/ARTLinearGradient.m +++ b/Libraries/ART/Brushes/ARTLinearGradient.m @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/ART/Brushes/ARTPattern.h b/Libraries/ART/Brushes/ARTPattern.h index ce1767cc9b0bc8..11fa9354de29d0 100644 --- a/Libraries/ART/Brushes/ARTPattern.h +++ b/Libraries/ART/Brushes/ARTPattern.h @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/ART/Brushes/ARTPattern.m b/Libraries/ART/Brushes/ARTPattern.m index 5c43586452d198..70e0bd25f367b7 100644 --- a/Libraries/ART/Brushes/ARTPattern.m +++ b/Libraries/ART/Brushes/ARTPattern.m @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/ART/Brushes/ARTRadialGradient.h b/Libraries/ART/Brushes/ARTRadialGradient.h index d7895f8b74d084..98bec026385014 100644 --- a/Libraries/ART/Brushes/ARTRadialGradient.h +++ b/Libraries/ART/Brushes/ARTRadialGradient.h @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/ART/Brushes/ARTRadialGradient.m b/Libraries/ART/Brushes/ARTRadialGradient.m index 19db9cb97b567a..c36f6ce57fc1fc 100644 --- a/Libraries/ART/Brushes/ARTRadialGradient.m +++ b/Libraries/ART/Brushes/ARTRadialGradient.m @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/ART/Brushes/ARTSolidColor.h b/Libraries/ART/Brushes/ARTSolidColor.h index a16c2b915c72f8..9880cfd61675ff 100644 --- a/Libraries/ART/Brushes/ARTSolidColor.h +++ b/Libraries/ART/Brushes/ARTSolidColor.h @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/ART/Brushes/ARTSolidColor.m b/Libraries/ART/Brushes/ARTSolidColor.m index 096c4e043ed429..2ef02d43d98592 100644 --- a/Libraries/ART/Brushes/ARTSolidColor.m +++ b/Libraries/ART/Brushes/ARTSolidColor.m @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/ART/RCTConvert+ART.h b/Libraries/ART/RCTConvert+ART.h index 1deb3c600176ab..5fe3b4edb281f9 100644 --- a/Libraries/ART/RCTConvert+ART.h +++ b/Libraries/ART/RCTConvert+ART.h @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -15,7 +15,7 @@ @interface RCTConvert (ART) -+ (CGPathRef)CGPath:(id)json; ++ (CGPathRef)CGPath:(id)json CF_RETURNS_NOT_RETAINED; + (CTTextAlignment)CTTextAlignment:(id)json; + (ARTTextFrame)ARTTextFrame:(id)json; + (ARTCGFloatArray)ARTCGFloatArray:(id)json; @@ -23,7 +23,7 @@ + (CGPoint)CGPoint:(id)json offset:(NSUInteger)offset; + (CGRect)CGRect:(id)json offset:(NSUInteger)offset; -+ (CGColorRef)CGColor:(id)json offset:(NSUInteger)offset; -+ (CGGradientRef)CGGradient:(id)json offset:(NSUInteger)offset; ++ (CGColorRef)CGColor:(id)json offset:(NSUInteger)offset CF_RETURNS_NOT_RETAINED; ++ (CGGradientRef)CGGradient:(id)json offset:(NSUInteger)offset CF_RETURNS_NOT_RETAINED; @end diff --git a/Libraries/ART/RCTConvert+ART.m b/Libraries/ART/RCTConvert+ART.m index ea22d5426f18f9..3312e319436369 100644 --- a/Libraries/ART/RCTConvert+ART.m +++ b/Libraries/ART/RCTConvert+ART.m @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/ART/ReactNativeART.js b/Libraries/ART/ReactNativeART.js index 5a08e748c8382c..1ac9a5e1468459 100644 --- a/Libraries/ART/ReactNativeART.js +++ b/Libraries/ART/ReactNativeART.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -19,7 +19,7 @@ const ReactNativeViewAttributes = require('ReactNativeViewAttributes'); const createReactNativeComponentClass = require('createReactNativeComponentClass'); const merge = require('merge'); -const invariant = require('fbjs/lib/invariant'); +const invariant = require('invariant'); // Diff Helpers @@ -399,7 +399,19 @@ function extractStrokeJoin(strokeJoin) { // Note: ART has a notion of width and height on Shape but AFAIK it's a noop in // ReactART. -class Shape extends React.Component { +export type ShapeProps = {| + fill?: mixed, + stroke?: mixed, + strokeCap?: mixed, + strokeDash?: mixed, + strokeJoin?: mixed, + strokeWidth?: mixed, + x?: number, + y?: number, + opacity?: mixed, +|}; + +class Shape extends React.Component { render() { const props = this.props; const path = props.d || childrenAsString(props.children); diff --git a/Libraries/ART/ViewManagers/ARTGroupManager.h b/Libraries/ART/ViewManagers/ARTGroupManager.h index a35e09481be639..f518cf0efbdbb3 100644 --- a/Libraries/ART/ViewManagers/ARTGroupManager.h +++ b/Libraries/ART/ViewManagers/ARTGroupManager.h @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/ART/ViewManagers/ARTGroupManager.m b/Libraries/ART/ViewManagers/ARTGroupManager.m index 2de0d0e5688739..9fb45d71bbdf4d 100644 --- a/Libraries/ART/ViewManagers/ARTGroupManager.m +++ b/Libraries/ART/ViewManagers/ARTGroupManager.m @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/ART/ViewManagers/ARTNodeManager.h b/Libraries/ART/ViewManagers/ARTNodeManager.h index 3a8f99e7abdc7a..84ef8fb01cf3d4 100644 --- a/Libraries/ART/ViewManagers/ARTNodeManager.h +++ b/Libraries/ART/ViewManagers/ARTNodeManager.h @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/ART/ViewManagers/ARTNodeManager.m b/Libraries/ART/ViewManagers/ARTNodeManager.m index 8e568ce6d8efd4..62680975ba1033 100644 --- a/Libraries/ART/ViewManagers/ARTNodeManager.m +++ b/Libraries/ART/ViewManagers/ARTNodeManager.m @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/ART/ViewManagers/ARTRenderableManager.h b/Libraries/ART/ViewManagers/ARTRenderableManager.h index 1e4b554f0f79f7..781ed32f8f574d 100644 --- a/Libraries/ART/ViewManagers/ARTRenderableManager.h +++ b/Libraries/ART/ViewManagers/ARTRenderableManager.h @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/ART/ViewManagers/ARTRenderableManager.m b/Libraries/ART/ViewManagers/ARTRenderableManager.m index a30841b50c1046..ad0264965391e5 100644 --- a/Libraries/ART/ViewManagers/ARTRenderableManager.m +++ b/Libraries/ART/ViewManagers/ARTRenderableManager.m @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/ART/ViewManagers/ARTShapeManager.h b/Libraries/ART/ViewManagers/ARTShapeManager.h index 2f31b23a0eab10..cc514b159ee192 100644 --- a/Libraries/ART/ViewManagers/ARTShapeManager.h +++ b/Libraries/ART/ViewManagers/ARTShapeManager.h @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/ART/ViewManagers/ARTShapeManager.m b/Libraries/ART/ViewManagers/ARTShapeManager.m index 57b603ca1a16b6..cb056e5d268877 100644 --- a/Libraries/ART/ViewManagers/ARTShapeManager.m +++ b/Libraries/ART/ViewManagers/ARTShapeManager.m @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/ART/ViewManagers/ARTSurfaceViewManager.h b/Libraries/ART/ViewManagers/ARTSurfaceViewManager.h index 5915536bd6f917..36d5949a90ab4d 100644 --- a/Libraries/ART/ViewManagers/ARTSurfaceViewManager.h +++ b/Libraries/ART/ViewManagers/ARTSurfaceViewManager.h @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/ART/ViewManagers/ARTSurfaceViewManager.m b/Libraries/ART/ViewManagers/ARTSurfaceViewManager.m index 54052a331d4585..1a03897937e860 100644 --- a/Libraries/ART/ViewManagers/ARTSurfaceViewManager.m +++ b/Libraries/ART/ViewManagers/ARTSurfaceViewManager.m @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/ART/ViewManagers/ARTTextManager.h b/Libraries/ART/ViewManagers/ARTTextManager.h index c7d5304da3a3eb..343e669af7553c 100644 --- a/Libraries/ART/ViewManagers/ARTTextManager.h +++ b/Libraries/ART/ViewManagers/ARTTextManager.h @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/ART/ViewManagers/ARTTextManager.m b/Libraries/ART/ViewManagers/ARTTextManager.m index ac6688c3d925b9..35c53811fd9509 100644 --- a/Libraries/ART/ViewManagers/ARTTextManager.m +++ b/Libraries/ART/ViewManagers/ARTTextManager.m @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/ActionSheetIOS/ActionSheetIOS.js b/Libraries/ActionSheetIOS/ActionSheetIOS.js index b135a23e5fc998..129de677d8b17d 100644 --- a/Libraries/ActionSheetIOS/ActionSheetIOS.js +++ b/Libraries/ActionSheetIOS/ActionSheetIOS.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -11,7 +11,7 @@ const RCTActionSheetManager = require('NativeModules').ActionSheetManager; -const invariant = require('fbjs/lib/invariant'); +const invariant = require('invariant'); const processColor = require('processColor'); /** @@ -27,7 +27,7 @@ const ActionSheetIOS = { * * - `options` (array of strings) - a list of button titles (required) * - `cancelButtonIndex` (int) - index of cancel button in `options` - * - `destructiveButtonIndex` (int) - index of destructive button in `options` + * - `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 * diff --git a/Libraries/ActionSheetIOS/RCTActionSheetManager.h b/Libraries/ActionSheetIOS/RCTActionSheetManager.h index b9a5b2df7329a1..53184041393cd4 100644 --- a/Libraries/ActionSheetIOS/RCTActionSheetManager.h +++ b/Libraries/ActionSheetIOS/RCTActionSheetManager.h @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/ActionSheetIOS/RCTActionSheetManager.m b/Libraries/ActionSheetIOS/RCTActionSheetManager.m index cd5d5719ff769c..9eca118126ca16 100644 --- a/Libraries/ActionSheetIOS/RCTActionSheetManager.m +++ b/Libraries/ActionSheetIOS/RCTActionSheetManager.m @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -32,20 +32,21 @@ - (dispatch_queue_t)methodQueue return dispatch_get_main_queue(); } -/* - * The `anchor` option takes a view to set as the anchor for the share - * popup to point to, on iPads running iOS 8. If it is not passed, it - * defaults to centering the share popup on screen without any arrows. - */ -- (CGRect)sourceRectInView:(UIView *)sourceView - anchorViewTag:(NSNumber *)anchorViewTag +- (void)presentViewController:(UIViewController *)alertController + onParentViewController:(UIViewController *)parentViewController + anchorViewTag:(NSNumber *)anchorViewTag { + alertController.modalPresentationStyle = UIModalPresentationPopover; + UIView *sourceView = parentViewController.view; + if (anchorViewTag) { - UIView *anchorView = [self.bridge.uiManager viewForReactTag:anchorViewTag]; - return [anchorView convertRect:anchorView.bounds toView:sourceView]; + sourceView = [self.bridge.uiManager viewForReactTag:anchorViewTag]; } else { - return (CGRect){sourceView.center, {1, 1}}; + alertController.popoverPresentationController.permittedArrowDirections = 0; } + alertController.popoverPresentationController.sourceView = sourceView; + alertController.popoverPresentationController.sourceRect = sourceView.bounds; + [parentViewController presentViewController:alertController animated:YES completion:nil]; } RCT_EXPORT_METHOD(showActionSheetWithOptions:(NSDictionary *)options @@ -63,8 +64,14 @@ - (CGRect)sourceRectInView:(UIView *)sourceView NSString *title = [RCTConvert NSString:options[@"title"]]; NSString *message = [RCTConvert NSString:options[@"message"]]; NSArray *buttons = [RCTConvert NSStringArray:options[@"options"]]; - NSInteger destructiveButtonIndex = options[@"destructiveButtonIndex"] ? [RCTConvert NSInteger:options[@"destructiveButtonIndex"]] : -1; NSInteger cancelButtonIndex = options[@"cancelButtonIndex"] ? [RCTConvert NSInteger:options[@"cancelButtonIndex"]] : -1; + NSArray *destructiveButtonIndices; + if ([options[@"destructiveButtonIndex"] isKindOfClass:[NSArray class]]) { + destructiveButtonIndices = [RCTConvert NSArray:options[@"destructiveButtonIndex"]]; + } else { + NSNumber *destructiveButtonIndex = options[@"destructiveButtonIndex"] ? [RCTConvert NSNumber:options[@"destructiveButtonIndex"]] : @-1; + destructiveButtonIndices = @[destructiveButtonIndex]; + } UIViewController *controller = RCTPresentedViewController(); @@ -79,9 +86,7 @@ - (CGRect)sourceRectInView:(UIView *)sourceView * defaults to centering the share popup on screen without any arrows. */ NSNumber *anchorViewTag = [RCTConvert NSNumber:options[@"anchor"]]; - UIView *sourceView = controller.view; - CGRect sourceRect = [self sourceRectInView:sourceView anchorViewTag:anchorViewTag]; - + UIAlertController *alertController = [UIAlertController alertControllerWithTitle:title message:message @@ -90,7 +95,7 @@ - (CGRect)sourceRectInView:(UIView *)sourceView NSInteger index = 0; for (NSString *option in buttons) { UIAlertActionStyle style = UIAlertActionStyleDefault; - if (index == destructiveButtonIndex) { + if ([destructiveButtonIndices containsObject:@(index)]) { style = UIAlertActionStyleDestructive; } else if (index == cancelButtonIndex) { style = UIAlertActionStyleCancel; @@ -106,15 +111,8 @@ - (CGRect)sourceRectInView:(UIView *)sourceView index++; } - alertController.modalPresentationStyle = UIModalPresentationPopover; - alertController.popoverPresentationController.sourceView = sourceView; - alertController.popoverPresentationController.sourceRect = sourceRect; - if (!anchorViewTag) { - alertController.popoverPresentationController.permittedArrowDirections = 0; - } - [controller presentViewController:alertController animated:YES completion:nil]; - alertController.view.tintColor = [RCTConvert UIColor:options[@"tintColor"]]; + [self presentViewController:alertController onParentViewController:controller anchorViewTag:anchorViewTag]; } RCT_EXPORT_METHOD(showShareActionSheetWithOptions:(NSDictionary *)options @@ -173,17 +171,10 @@ - (CGRect)sourceRectInView:(UIView *)sourceView } }; - shareController.modalPresentationStyle = UIModalPresentationPopover; NSNumber *anchorViewTag = [RCTConvert NSNumber:options[@"anchor"]]; - if (!anchorViewTag) { - shareController.popoverPresentationController.permittedArrowDirections = 0; - } - shareController.popoverPresentationController.sourceView = controller.view; - shareController.popoverPresentationController.sourceRect = [self sourceRectInView:controller.view anchorViewTag:anchorViewTag]; - - [controller presentViewController:shareController animated:YES completion:nil]; - shareController.view.tintColor = [RCTConvert UIColor:options[@"tintColor"]]; + + [self presentViewController:shareController onParentViewController:controller anchorViewTag:anchorViewTag]; } @end diff --git a/Libraries/Alert/Alert.js b/Libraries/Alert/Alert.js index a424f5026d5a8c..ddc126516cb24f 100644 --- a/Libraries/Alert/Alert.js +++ b/Libraries/Alert/Alert.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -10,12 +10,10 @@ 'use strict'; -const AlertIOS = require('AlertIOS'); const NativeModules = require('NativeModules'); +const RCTAlertManager = NativeModules.AlertManager; const Platform = require('Platform'); -import type {AlertType, AlertButtonStyle} from 'AlertIOS'; - export type Buttons = Array<{ text?: string, onPress?: ?Function, @@ -27,95 +25,155 @@ type Options = { onDismiss?: ?Function, }; +type AlertType = $Enum<{ + default: string, + 'plain-text': string, + 'secure-text': string, + 'login-password': string, +}>; + +export type AlertButtonStyle = $Enum<{ + default: string, + cancel: string, + destructive: string, +}>; + /** * Launches an alert dialog with the specified title and message. * * See http://facebook.github.io/react-native/docs/alert.html */ class Alert { - /** - * Launches an alert dialog with the specified title and message. - * - * See http://facebook.github.io/react-native/docs/alert.html#alert - */ static alert( title: ?string, message?: ?string, buttons?: Buttons, options?: Options, - type?: AlertType, ): void { if (Platform.OS === 'ios') { - if (typeof type !== 'undefined') { - console.warn( - 'Alert.alert() with a 5th "type" parameter is deprecated and will be removed. Use AlertIOS.prompt() instead.', - ); - AlertIOS.alert(title, message, buttons, type); - return; - } - AlertIOS.alert(title, message, buttons); + Alert.prompt(title, message, buttons, 'default'); } else if (Platform.OS === 'android') { - AlertAndroid.alert(title, message, buttons, options); + let config = { + title: title || '', + message: message || '', + }; + + if (options) { + config = {...config, cancelable: options.cancelable}; + } + // At most three buttons (neutral, negative, positive). Ignore rest. + // The text 'OK' should be probably localized. iOS Alert does that in native. + const validButtons: Buttons = buttons + ? buttons.slice(0, 3) + : [{text: 'OK'}]; + const buttonPositive = validButtons.pop(); + const buttonNegative = validButtons.pop(); + const buttonNeutral = validButtons.pop(); + if (buttonNeutral) { + config = {...config, buttonNeutral: buttonNeutral.text || ''}; + } + if (buttonNegative) { + config = {...config, buttonNegative: buttonNegative.text || ''}; + } + if (buttonPositive) { + config = {...config, buttonPositive: buttonPositive.text || ''}; + } + NativeModules.DialogManagerAndroid.showAlert( + config, + errorMessage => console.warn(errorMessage), + (action, buttonKey) => { + if (action === NativeModules.DialogManagerAndroid.buttonClicked) { + if ( + buttonKey === NativeModules.DialogManagerAndroid.buttonNeutral + ) { + buttonNeutral.onPress && buttonNeutral.onPress(); + } else if ( + buttonKey === NativeModules.DialogManagerAndroid.buttonNegative + ) { + buttonNegative.onPress && buttonNegative.onPress(); + } else if ( + buttonKey === NativeModules.DialogManagerAndroid.buttonPositive + ) { + buttonPositive.onPress && buttonPositive.onPress(); + } + } else if (action === NativeModules.DialogManagerAndroid.dismissed) { + options && options.onDismiss && options.onDismiss(); + } + }, + ); } } -} -/** - * Wrapper around the Android native module. - */ -class AlertAndroid { - static alert( + static prompt( title: ?string, message?: ?string, - buttons?: Buttons, - options?: Options, + callbackOrButtons?: ?(((text: string) => void) | Buttons), + type?: ?AlertType = 'plain-text', + defaultValue?: string, + keyboardType?: string, ): void { - let config = { - title: title || '', - message: message || '', - }; + if (Platform.OS === 'ios') { + if (typeof type === 'function') { + console.warn( + 'You passed a callback function as the "type" argument to Alert.prompt(). React Native is ' + + 'assuming you want to use the deprecated Alert.prompt(title, defaultValue, buttons, callback) ' + + 'signature. The current signature is Alert.prompt(title, message, callbackOrButtons, type, defaultValue, ' + + 'keyboardType) and the old syntax will be removed in a future version.', + ); - if (options) { - config = {...config, cancelable: options.cancelable}; - } - // At most three buttons (neutral, negative, positive). Ignore rest. - // The text 'OK' should be probably localized. iOS Alert does that in native. - const validButtons: Buttons = buttons - ? buttons.slice(0, 3) - : [{text: 'OK'}]; - const buttonPositive = validButtons.pop(); - const buttonNegative = validButtons.pop(); - const buttonNeutral = validButtons.pop(); - if (buttonNeutral) { - config = {...config, buttonNeutral: buttonNeutral.text || ''}; - } - if (buttonNegative) { - config = {...config, buttonNegative: buttonNegative.text || ''}; - } - if (buttonPositive) { - config = {...config, buttonPositive: buttonPositive.text || ''}; - } - NativeModules.DialogManagerAndroid.showAlert( - config, - errorMessage => console.warn(errorMessage), - (action, buttonKey) => { - if (action === NativeModules.DialogManagerAndroid.buttonClicked) { - if (buttonKey === NativeModules.DialogManagerAndroid.buttonNeutral) { - buttonNeutral.onPress && buttonNeutral.onPress(); - } else if ( - buttonKey === NativeModules.DialogManagerAndroid.buttonNegative - ) { - buttonNegative.onPress && buttonNegative.onPress(); - } else if ( - buttonKey === NativeModules.DialogManagerAndroid.buttonPositive - ) { - buttonPositive.onPress && buttonPositive.onPress(); + const callback = type; + RCTAlertManager.alertWithArgs( + { + title: title || '', + type: 'plain-text', + defaultValue: message, + }, + (id, value) => { + callback(value); + }, + ); + return; + } + + let callbacks = []; + const buttons = []; + let cancelButtonKey; + let destructiveButtonKey; + if (typeof callbackOrButtons === 'function') { + callbacks = [callbackOrButtons]; + } else if (Array.isArray(callbackOrButtons)) { + callbackOrButtons.forEach((btn, index) => { + callbacks[index] = btn.onPress; + if (btn.style === 'cancel') { + cancelButtonKey = String(index); + } else if (btn.style === 'destructive') { + destructiveButtonKey = String(index); } - } else if (action === NativeModules.DialogManagerAndroid.dismissed) { - options && options.onDismiss && options.onDismiss(); - } - }, - ); + if (btn.text || index < (callbackOrButtons || []).length - 1) { + const btnDef = {}; + btnDef[index] = btn.text || ''; + buttons.push(btnDef); + } + }); + } + + RCTAlertManager.alertWithArgs( + { + title: title || '', + message: message || undefined, + buttons, + type: type || undefined, + defaultValue, + cancelButtonKey, + destructiveButtonKey, + keyboardType, + }, + (id, value) => { + const cb = callbacks[id]; + cb && cb(value); + }, + ); + } } } diff --git a/Libraries/Alert/AlertIOS.js b/Libraries/Alert/AlertIOS.js deleted file mode 100644 index 1998865b2d597c..00000000000000 --- a/Libraries/Alert/AlertIOS.js +++ /dev/null @@ -1,181 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @format - * @flow - * @jsdoc - */ - -'use strict'; - -const RCTAlertManager = require('NativeModules').AlertManager; - -/** - * An Alert button type - */ -export type AlertType = $Enum<{ - /** - * Default alert with no inputs - */ - default: string, - /** - * Plain text input alert - */ - 'plain-text': string, - /** - * Secure text input alert - */ - 'secure-text': string, - /** - * Login and password alert - */ - 'login-password': string, -}>; - -/** - * An Alert button style - */ -export type AlertButtonStyle = $Enum<{ - /** - * Default button style - */ - default: string, - /** - * Cancel button style - */ - cancel: string, - /** - * Destructive button style - */ - destructive: string, -}>; - -/** - * Array or buttons - * @typedef {Array} ButtonsArray - * @property {string=} text Button label - * @property {Function=} onPress Callback function when button pressed - * @property {AlertButtonStyle=} style Button style - */ -export type ButtonsArray = Array<{ - /** - * Button label - */ - text?: string, - /** - * Callback function when button pressed - */ - onPress?: ?Function, - /** - * Button style - */ - style?: AlertButtonStyle, -}>; - -/** - * Use `AlertIOS` to display an alert dialog with a message or to create a prompt for user input on iOS. If you don't need to prompt for user input, we recommend using `Alert.alert() for cross-platform support. - * - * See http://facebook.github.io/react-native/docs/alertios.html - */ -class AlertIOS { - /** - * Create and display a popup alert. - * - * See http://facebook.github.io/react-native/docs/alertios.html#alert - */ - static alert( - title: ?string, - message?: ?string, - callbackOrButtons?: ?((() => void) | ButtonsArray), - type?: AlertType, - ): void { - if (typeof type !== 'undefined') { - console.warn( - 'AlertIOS.alert() with a 4th "type" parameter is deprecated and will be removed. Use AlertIOS.prompt() instead.', - ); - this.prompt(title, message, callbackOrButtons, type); - return; - } - this.prompt(title, message, callbackOrButtons, 'default'); - } - - /** - * Create and display a prompt to enter some text. - * - * See http://facebook.github.io/react-native/docs/alertios.html#prompt - */ - static prompt( - title: ?string, - message?: ?string, - callbackOrButtons?: ?(((text: string) => void) | ButtonsArray), - type?: ?AlertType = 'plain-text', - defaultValue?: string, - keyboardType?: string, - ): void { - if (typeof type === 'function') { - console.warn( - 'You passed a callback function as the "type" argument to AlertIOS.prompt(). React Native is ' + - 'assuming you want to use the deprecated AlertIOS.prompt(title, defaultValue, buttons, callback) ' + - 'signature. The current signature is AlertIOS.prompt(title, message, callbackOrButtons, type, defaultValue, ' + - 'keyboardType) and the old syntax will be removed in a future version.', - ); - - const callback = type; - RCTAlertManager.alertWithArgs( - { - title: title || '', - type: 'plain-text', - defaultValue: message, - }, - (id, value) => { - callback(value); - }, - ); - return; - } - - let callbacks = []; - const buttons = []; - let cancelButtonKey; - let destructiveButtonKey; - if (typeof callbackOrButtons === 'function') { - callbacks = [callbackOrButtons]; - } else if (callbackOrButtons instanceof Array) { - callbackOrButtons.forEach((btn, index) => { - callbacks[index] = btn.onPress; - if (btn.style === 'cancel') { - cancelButtonKey = String(index); - } else if (btn.style === 'destructive') { - destructiveButtonKey = String(index); - } - if (btn.text || index < (callbackOrButtons || []).length - 1) { - const btnDef = {}; - btnDef[index] = btn.text || ''; - buttons.push(btnDef); - } - }); - } - - RCTAlertManager.alertWithArgs( - { - title: title || '', - message: message || undefined, - buttons, - type: type || undefined, - defaultValue, - cancelButtonKey, - destructiveButtonKey, - keyboardType, - }, - (id, value) => { - const cb = callbacks[id]; - cb && cb(value); - }, - ); - } -} - -module.exports = AlertIOS; diff --git a/Libraries/Alert/RCTAlertManager.android.js b/Libraries/Alert/RCTAlertManager.android.js index 2e510add8ddaca..b16298f01ff3f2 100644 --- a/Libraries/Alert/RCTAlertManager.android.js +++ b/Libraries/Alert/RCTAlertManager.android.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2013-present, Facebook, Inc. + * 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. diff --git a/Libraries/Alert/RCTAlertManager.ios.js b/Libraries/Alert/RCTAlertManager.ios.js index 529bb5da0e55f2..a300de65521d97 100644 --- a/Libraries/Alert/RCTAlertManager.ios.js +++ b/Libraries/Alert/RCTAlertManager.ios.js @@ -1,11 +1,11 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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 + * @flow strict-local */ 'use strict'; diff --git a/Libraries/Animated/examples/style.css b/Libraries/Animated/examples/style.css index c188ad79b07fd2..24191a56b985ca 100644 --- a/Libraries/Animated/examples/style.css +++ b/Libraries/Animated/examples/style.css @@ -1,3 +1,10 @@ +/** + * 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. + */ + html, h1, h2 { font-family: 'Roboto', sans-serif; font-weight: 300; diff --git a/Libraries/Animated/release/.gitignore b/Libraries/Animated/release/.gitignore new file mode 100644 index 00000000000000..4c422f3bc92283 --- /dev/null +++ b/Libraries/Animated/release/.gitignore @@ -0,0 +1,3 @@ +/lib/ +/dist/ +/node_modules/ diff --git a/Libraries/Animated/release/gulpfile.js b/Libraries/Animated/release/gulpfile.js index b5599e9365fef3..291f3236303d87 100644 --- a/Libraries/Animated/release/gulpfile.js +++ b/Libraries/Animated/release/gulpfile.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2013-present, Facebook, Inc. + * 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. diff --git a/Libraries/Animated/release/package.json b/Libraries/Animated/release/package.json index 49f6a2081ffd14..cccaf09d35f896 100644 --- a/Libraries/Animated/release/package.json +++ b/Libraries/Animated/release/package.json @@ -10,7 +10,7 @@ "license": "MIT", "main": "Animated.js", "dependencies": { - "fbjs": "0.8.17" + "fbjs": "^1.0.0" }, "scripts": { "build": "gulp" @@ -19,7 +19,7 @@ "babel-core": "^5.8.25", "babel-loader": "^5.3.2", "del": "^1.2.0", - "fbjs-scripts": "^0.2.0", + "fbjs-scripts": "^1.0.0", "gulp": "^3.9.0", "gulp-babel": "^5.1.0", "gulp-derequire": "^2.1.0", diff --git a/Libraries/Animated/src/Animated.js b/Libraries/Animated/src/Animated.js index 5b831396c3fc74..41b5173f543228 100644 --- a/Libraries/Animated/src/Animated.js +++ b/Libraries/Animated/src/Animated.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -10,20 +10,30 @@ 'use strict'; -const AnimatedImplementation = require('AnimatedImplementation'); -const FlatList = require('FlatList'); -const Image = require('Image'); -const ScrollView = require('ScrollView'); -const SectionList = require('SectionList'); -const Text = require('Text'); -const View = require('View'); +import Platform from 'Platform'; + +const AnimatedImplementation = Platform.isTesting + ? require('AnimatedMock') + : require('AnimatedImplementation'); module.exports = { + get FlatList() { + return require('AnimatedFlatList'); + }, + get Image() { + return require('AnimatedImage'); + }, + get ScrollView() { + return require('AnimatedScrollView'); + }, + get SectionList() { + return require('AnimatedSectionList'); + }, + get Text() { + return require('AnimatedText'); + }, + get View() { + return require('AnimatedView'); + }, ...AnimatedImplementation, - View: AnimatedImplementation.createAnimatedComponent(View), - Text: AnimatedImplementation.createAnimatedComponent(Text), - Image: AnimatedImplementation.createAnimatedComponent(Image), - ScrollView: AnimatedImplementation.createAnimatedComponent(ScrollView), - FlatList: AnimatedImplementation.createAnimatedComponent(FlatList), - SectionList: AnimatedImplementation.createAnimatedComponent(SectionList), }; diff --git a/Libraries/Animated/src/AnimatedEvent.js b/Libraries/Animated/src/AnimatedEvent.js index 084632737c661d..72ef2af980bfb0 100644 --- a/Libraries/Animated/src/AnimatedEvent.js +++ b/Libraries/Animated/src/AnimatedEvent.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -13,7 +13,7 @@ const AnimatedValue = require('./nodes/AnimatedValue'); const NativeAnimatedHelper = require('./NativeAnimatedHelper'); const ReactNative = require('ReactNative'); -const invariant = require('fbjs/lib/invariant'); +const invariant = require('invariant'); const {shouldUseNativeDriver} = require('./NativeAnimatedHelper'); export type Mapping = {[key: string]: Mapping} | AnimatedValue; @@ -158,7 +158,7 @@ class AnimatedEvent { }; } - _callListeners(...args) { + _callListeners(...args: any) { this._listeners.forEach(listener => listener(...args)); } diff --git a/Libraries/Animated/src/AnimatedImplementation.js b/Libraries/Animated/src/AnimatedImplementation.js index 2c1ce431d10686..88f7c02c045880 100644 --- a/Libraries/Animated/src/AnimatedImplementation.js +++ b/Libraries/Animated/src/AnimatedImplementation.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -514,6 +514,8 @@ const event = function(argMapping: Array, config?: EventConfig): any { * easy to build and maintain. `Animated` focuses on declarative relationships * between inputs and outputs, with configurable transforms in between, and * simple `start`/`stop` methods to control time-based animation execution. + * If additional transforms are added, be sure to include them in + * AnimatedMock.js as well. * * See http://facebook.github.io/react-native/docs/animated.html */ @@ -685,5 +687,10 @@ module.exports = { forkEvent, unforkEvent, + /** + * Expose Event class, so it can be used as a type for type checkers. + */ + Event: AnimatedEvent, + __PropsOnlyForTests: AnimatedProps, }; diff --git a/Libraries/Animated/src/AnimatedMock.js b/Libraries/Animated/src/AnimatedMock.js new file mode 100644 index 00000000000000..6efcdd1815a9ae --- /dev/null +++ b/Libraries/Animated/src/AnimatedMock.js @@ -0,0 +1,152 @@ +/** + * 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 + * @format + */ +'use strict'; + +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'); + +const createAnimatedComponent = require('./createAnimatedComponent'); + +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 + * animation functions from AnimatedImplementation with empty animations for + * predictability in tests. + */ +type CompositeAnimation = { + start: (callback?: ?EndCallback) => void, + stop: () => void, + reset: () => void, + _startNativeLoop: (iterations?: number) => void, + _isUsingNativeDriver: () => boolean, +}; + +const emptyAnimation = { + start: () => {}, + stop: () => {}, + reset: () => {}, + _startNativeLoop: () => {}, + _isUsingNativeDriver: () => { + return false; + }, +}; + +const spring = function( + value: AnimatedValue | AnimatedValueXY, + config: SpringAnimationConfig, +): CompositeAnimation { + const anyValue: any = value; + return { + ...emptyAnimation, + start: (callback?: ?EndCallback): void => { + anyValue.setValue(config.toValue); + callback && callback({finished: true}); + }, + }; +}; + +const timing = function( + value: AnimatedValue | AnimatedValueXY, + config: TimingAnimationConfig, +): CompositeAnimation { + const anyValue: any = value; + return { + ...emptyAnimation, + start: (callback?: ?EndCallback): void => { + anyValue.setValue(config.toValue); + callback && callback({finished: true}); + }, + }; +}; + +const decay = function( + value: AnimatedValue | AnimatedValueXY, + config: DecayAnimationConfig, +): CompositeAnimation { + return emptyAnimation; +}; + +const sequence = function( + animations: Array, +): CompositeAnimation { + return emptyAnimation; +}; + +type ParallelConfig = { + stopTogether?: boolean, +}; +const parallel = function( + animations: Array, + config?: ?ParallelConfig, +): CompositeAnimation { + return emptyAnimation; +}; + +const delay = function(time: number): CompositeAnimation { + return emptyAnimation; +}; + +const stagger = function( + time: number, + animations: Array, +): CompositeAnimation { + return emptyAnimation; +}; + +type LoopAnimationConfig = {iterations: number}; + +const loop = function( + animation: CompositeAnimation, + {iterations = -1}: LoopAnimationConfig = {}, +): CompositeAnimation { + return emptyAnimation; +}; + +const event = function(argMapping: Array, config?: EventConfig): any { + return null; +}; + +module.exports = { + Value: AnimatedValue, + ValueXY: AnimatedValueXY, + Interpolation: AnimatedInterpolation, + Node: AnimatedNode, + decay, + timing, + spring, + add: AnimatedImplementation.add, + subtract: AnimatedImplementation.subtract, + divide: AnimatedImplementation.divide, + multiply: AnimatedImplementation.multiply, + modulo: AnimatedImplementation.modulo, + diffClamp: AnimatedImplementation.diffClamp, + delay, + sequence, + parallel, + stagger, + loop, + event, + createAnimatedComponent, + attachNativeEvent, + forkEvent: AnimatedImplementation.forkEvent, + unforkEvent: AnimatedImplementation.unforkEvent, + Event: AnimatedEvent, + __PropsOnlyForTests: AnimatedProps, +}; diff --git a/Libraries/Animated/src/AnimatedWeb.js b/Libraries/Animated/src/AnimatedWeb.js index e80023d73687a4..cf105d4af58a6c 100644 --- a/Libraries/Animated/src/AnimatedWeb.js +++ b/Libraries/Animated/src/AnimatedWeb.js @@ -1,11 +1,11 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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 + * @flow strict-local */ 'use strict'; diff --git a/Libraries/Animated/src/Easing.js b/Libraries/Animated/src/Easing.js index 94fb865882c040..9932038e6329fd 100644 --- a/Libraries/Animated/src/Easing.js +++ b/Libraries/Animated/src/Easing.js @@ -1,11 +1,11 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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 + * @flow strict */ 'use strict'; @@ -175,10 +175,7 @@ class Easing { * * - http://tiny.cc/back_default (s = 1.70158, default) */ - static back(s: number): (t: number) => number { - if (s === undefined) { - s = 1.70158; - } + static back(s: number = 1.70158): (t: number) => number { return t => t * t * ((s + 1) * t - s); } @@ -193,17 +190,17 @@ class Easing { } if (t < 2 / 2.75) { - t -= 1.5 / 2.75; - return 7.5625 * t * t + 0.75; + const t2 = t - 1.5 / 2.75; + return 7.5625 * t2 * t2 + 0.75; } if (t < 2.5 / 2.75) { - t -= 2.25 / 2.75; - return 7.5625 * t * t + 0.9375; + const t2 = t - 2.25 / 2.75; + return 7.5625 * t2 * t2 + 0.9375; } - t -= 2.625 / 2.75; - return 7.5625 * t * t + 0.984375; + const t2 = t - 2.625 / 2.75; + return 7.5625 * t2 * t2 + 0.984375; } /** diff --git a/Libraries/Animated/src/NativeAnimatedHelper.js b/Libraries/Animated/src/NativeAnimatedHelper.js index 3142ea24c084d1..2ac39f024ab63a 100644 --- a/Libraries/Animated/src/NativeAnimatedHelper.js +++ b/Libraries/Animated/src/NativeAnimatedHelper.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -12,7 +12,7 @@ const NativeAnimatedModule = require('NativeModules').NativeAnimatedModule; const NativeEventEmitter = require('NativeEventEmitter'); -const invariant = require('fbjs/lib/invariant'); +const invariant = require('invariant'); import type {AnimationConfig} from './animations/Animation'; import type {EventConfig} from './AnimatedEvent'; @@ -153,6 +153,7 @@ const STYLES_WHITELIST = { borderTopLeftRadius: true, borderTopRightRadius: true, borderTopStartRadius: true, + elevation: true, /* ios styles */ shadowOpacity: true, shadowRadius: true, @@ -259,6 +260,22 @@ function shouldUseNativeDriver(config: AnimationConfig | EventConfig): boolean { return config.useNativeDriver || false; } +function transformDataType(value: any): number { + // Change the string type to number type so we can reuse the same logic in + // iOS and Android platform + if (typeof value !== 'string') { + return value; + } + if (/deg$/.test(value)) { + const degrees = parseFloat(value) || 0; + const radians = (degrees * Math.PI) / 180.0; + return radians; + } else { + // Assume radians + return parseFloat(value) || 0; + } +} + module.exports = { API, addWhitelistedStyleProp, @@ -271,6 +288,7 @@ module.exports = { generateNewAnimationId, assertNativeAnimatedModule, shouldUseNativeDriver, + transformDataType, get nativeEventEmitter() { if (!nativeEventEmitter) { nativeEventEmitter = new NativeEventEmitter(NativeAnimatedModule); diff --git a/Libraries/Animated/src/SpringConfig.js b/Libraries/Animated/src/SpringConfig.js index 456af9c40ec1ae..e226a3173a1902 100644 --- a/Libraries/Animated/src/SpringConfig.js +++ b/Libraries/Animated/src/SpringConfig.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/Animated/src/__tests__/Animated-test.js b/Libraries/Animated/src/__tests__/Animated-test.js index 6c5ef66b14a170..930d8a020f2bff 100644 --- a/Libraries/Animated/src/__tests__/Animated-test.js +++ b/Libraries/Animated/src/__tests__/Animated-test.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -134,7 +134,9 @@ describe('Animated tests', () => { expect(callback).toBeCalled(); }); - it('send toValue when an underdamped spring stops', () => { + // This test is flaky and we are asking open source to fix it + // https://github.com/facebook/react-native/issues/21517 + it.skip('send toValue when an underdamped spring stops', () => { const anim = new Animated.Value(0); const listener = jest.fn(); anim.addListener(listener); diff --git a/Libraries/Animated/src/__tests__/AnimatedMock-test.js b/Libraries/Animated/src/__tests__/AnimatedMock-test.js new file mode 100644 index 00000000000000..ac0bc6b20b36cd --- /dev/null +++ b/Libraries/Animated/src/__tests__/AnimatedMock-test.js @@ -0,0 +1,29 @@ +/** + * 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 AnimatedMock = require('AnimatedMock'); +const AnimatedImplementation = require('AnimatedImplementation'); + +describe('Animated Mock', () => { + it('matches implementation keys', () => { + expect(Object.keys(AnimatedMock)).toEqual( + Object.keys(AnimatedImplementation), + ); + }); + it('matches implementation params', () => { + Object.keys(AnimatedImplementation).forEach(key => + expect(AnimatedImplementation[key].length).toEqual( + AnimatedMock[key].length, + ), + ); + }); +}); diff --git a/Libraries/Animated/src/__tests__/AnimatedNative-test.js b/Libraries/Animated/src/__tests__/AnimatedNative-test.js index f9f3836835fc47..fb6257f15d8183 100644 --- a/Libraries/Animated/src/__tests__/AnimatedNative-test.js +++ b/Libraries/Animated/src/__tests__/AnimatedNative-test.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/Animated/src/__tests__/Easing-test.js b/Libraries/Animated/src/__tests__/Easing-test.js index 0284f3f0b697ef..1cb6152538fec2 100644 --- a/Libraries/Animated/src/__tests__/Easing-test.js +++ b/Libraries/Animated/src/__tests__/Easing-test.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/Animated/src/__tests__/Interpolation-test.js b/Libraries/Animated/src/__tests__/Interpolation-test.js index 5515f09bacd0bc..6b77584b00d8ab 100644 --- a/Libraries/Animated/src/__tests__/Interpolation-test.js +++ b/Libraries/Animated/src/__tests__/Interpolation-test.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/Animated/src/__tests__/bezier-test.js b/Libraries/Animated/src/__tests__/bezier-test.js index e9f00a7d214878..360d6262f91144 100644 --- a/Libraries/Animated/src/__tests__/bezier-test.js +++ b/Libraries/Animated/src/__tests__/bezier-test.js @@ -1,4 +1,9 @@ /** + * 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. + * * BezierEasing - use bezier curve for transition easing function * https://github.com/gre/bezier-easing * diff --git a/Libraries/Animated/src/animations/Animation.js b/Libraries/Animated/src/animations/Animation.js index b4e38016dba557..b34ac9555a1eca 100644 --- a/Libraries/Animated/src/animations/Animation.js +++ b/Libraries/Animated/src/animations/Animation.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/Animated/src/animations/DecayAnimation.js b/Libraries/Animated/src/animations/DecayAnimation.js index a6fe05bb3f6343..0e09b6144d30e1 100644 --- a/Libraries/Animated/src/animations/DecayAnimation.js +++ b/Libraries/Animated/src/animations/DecayAnimation.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/Animated/src/animations/SpringAnimation.js b/Libraries/Animated/src/animations/SpringAnimation.js index a0f38d3b024e5c..497e72a2e35f0b 100644 --- a/Libraries/Animated/src/animations/SpringAnimation.js +++ b/Libraries/Animated/src/animations/SpringAnimation.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -14,7 +14,7 @@ const AnimatedValueXY = require('../nodes/AnimatedValueXY'); const Animation = require('./Animation'); const SpringConfig = require('../SpringConfig'); -const invariant = require('fbjs/lib/invariant'); +const invariant = require('invariant'); const {shouldUseNativeDriver} = require('../NativeAnimatedHelper'); import type {AnimationConfig, EndCallback} from './Animation'; diff --git a/Libraries/Animated/src/animations/TimingAnimation.js b/Libraries/Animated/src/animations/TimingAnimation.js index 694a52c0a39f7b..61ad74e9e1f5f0 100644 --- a/Libraries/Animated/src/animations/TimingAnimation.js +++ b/Libraries/Animated/src/animations/TimingAnimation.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/Animated/src/bezier.js b/Libraries/Animated/src/bezier.js index 15f8e2dc1b686f..727872b848aacd 100644 --- a/Libraries/Animated/src/bezier.js +++ b/Libraries/Animated/src/bezier.js @@ -1,8 +1,13 @@ /** + * 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. + * * BezierEasing - use bezier curve for transition easing function * https://github.com/gre/bezier-easing * - * @flow + * @flow strict * @format * @copyright 2014-2015 GaĆ«tan Renaudeau. MIT License. */ @@ -40,10 +45,12 @@ function getSlope(aT, aA1, aA2) { return 3.0 * A(aA1, aA2) * aT * aT + 2.0 * B(aA1, aA2) * aT + C(aA1); } -function binarySubdivide(aX, aA, aB, mX1, mX2) { +function binarySubdivide(aX, _aA, _aB, mX1, mX2) { let currentX, currentT, - i = 0; + i = 0, + aA = _aA, + aB = _aB; do { currentT = aA + (aB - aA) / 2.0; currentX = calcBezier(currentT, mX1, mX2) - aX; @@ -59,7 +66,8 @@ function binarySubdivide(aX, aA, aB, mX1, mX2) { return currentT; } -function newtonRaphsonIterate(aX, aGuessT, mX1, mX2) { +function newtonRaphsonIterate(aX, _aGuessT, mX1, mX2) { + let aGuessT = _aGuessT; for (let i = 0; i < NEWTON_ITERATIONS; ++i) { const currentSlope = getSlope(aGuessT, mX1, mX2); if (currentSlope === 0.0) { @@ -77,7 +85,7 @@ module.exports = function bezier( mX2: number, mY2: number, ) { - if (!(0 <= mX1 && mX1 <= 1 && 0 <= mX2 && mX2 <= 1)) { + if (!(mX1 >= 0 && mX1 <= 1 && mX2 >= 0 && mX2 <= 1)) { throw new Error('bezier x values must be in [0, 1] range'); } diff --git a/Libraries/Animated/src/components/AnimatedFlatList.js b/Libraries/Animated/src/components/AnimatedFlatList.js new file mode 100644 index 00000000000000..af69ff3674769e --- /dev/null +++ b/Libraries/Animated/src/components/AnimatedFlatList.js @@ -0,0 +1,17 @@ +/** + * 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 FlatList = require('FlatList'); + +const createAnimatedComponent = require('createAnimatedComponent'); + +module.exports = createAnimatedComponent(FlatList); diff --git a/Libraries/Animated/src/components/AnimatedImage.js b/Libraries/Animated/src/components/AnimatedImage.js new file mode 100644 index 00000000000000..e7d0bc4429513a --- /dev/null +++ b/Libraries/Animated/src/components/AnimatedImage.js @@ -0,0 +1,17 @@ +/** + * 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 Image = require('Image'); + +const createAnimatedComponent = require('createAnimatedComponent'); + +module.exports = createAnimatedComponent(Image); diff --git a/Libraries/Animated/src/components/AnimatedScrollView.js b/Libraries/Animated/src/components/AnimatedScrollView.js new file mode 100644 index 00000000000000..195f17b7005ad7 --- /dev/null +++ b/Libraries/Animated/src/components/AnimatedScrollView.js @@ -0,0 +1,17 @@ +/** + * 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 ScrollView = require('ScrollView'); + +const createAnimatedComponent = require('createAnimatedComponent'); + +module.exports = createAnimatedComponent(ScrollView); diff --git a/Libraries/Animated/src/components/AnimatedSectionList.js b/Libraries/Animated/src/components/AnimatedSectionList.js new file mode 100644 index 00000000000000..482e203abac637 --- /dev/null +++ b/Libraries/Animated/src/components/AnimatedSectionList.js @@ -0,0 +1,17 @@ +/** + * 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 SectionList = require('SectionList'); + +const createAnimatedComponent = require('createAnimatedComponent'); + +module.exports = createAnimatedComponent(SectionList); diff --git a/Libraries/Animated/src/components/AnimatedText.js b/Libraries/Animated/src/components/AnimatedText.js new file mode 100644 index 00000000000000..870ba144b10207 --- /dev/null +++ b/Libraries/Animated/src/components/AnimatedText.js @@ -0,0 +1,17 @@ +/** + * 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 Text = require('Text'); + +const createAnimatedComponent = require('createAnimatedComponent'); + +module.exports = createAnimatedComponent(Text); diff --git a/Libraries/Animated/src/components/AnimatedView.js b/Libraries/Animated/src/components/AnimatedView.js new file mode 100644 index 00000000000000..396760d999485f --- /dev/null +++ b/Libraries/Animated/src/components/AnimatedView.js @@ -0,0 +1,17 @@ +/** + * 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 View = require('View'); + +const createAnimatedComponent = require('createAnimatedComponent'); + +module.exports = createAnimatedComponent(View); diff --git a/Libraries/Animated/src/createAnimatedComponent.js b/Libraries/Animated/src/createAnimatedComponent.js index a4177d65be0e40..327070de59da40 100644 --- a/Libraries/Animated/src/createAnimatedComponent.js +++ b/Libraries/Animated/src/createAnimatedComponent.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -12,9 +12,9 @@ const {AnimatedEvent} = require('./AnimatedEvent'); const AnimatedProps = require('./nodes/AnimatedProps'); const React = require('React'); -const ViewStylePropTypes = require('ViewStylePropTypes'); +const DeprecatedViewStylePropTypes = require('DeprecatedViewStylePropTypes'); -const invariant = require('fbjs/lib/invariant'); +const invariant = require('invariant'); function createAnimatedComponent(Component: any): any { invariant( @@ -30,13 +30,11 @@ function createAnimatedComponent(Component: any): any { _prevComponent: any; _propsAnimated: AnimatedProps; _eventDetachers: Array = []; - _setComponentRef: Function; static __skipSetNativeProps_FOR_TESTS_ONLY = false; constructor(props: Object) { super(props); - this._setComponentRef = this._setComponentRef.bind(this); } componentWillUnmount() { @@ -164,10 +162,10 @@ function createAnimatedComponent(Component: any): any { ); } - _setComponentRef(c) { + _setComponentRef = c => { this._prevComponent = this._component; this._component = c; - } + }; // A third party library can use getNode() // to get the node reference of the decorated component @@ -184,7 +182,7 @@ function createAnimatedComponent(Component: any): any { return; } - for (const key in ViewStylePropTypes) { + for (const key in DeprecatedViewStylePropTypes) { if (!propTypes[key] && props[key] !== undefined) { console.warn( 'You are setting the style `{ ' + diff --git a/Libraries/Animated/src/nodes/AnimatedAddition.js b/Libraries/Animated/src/nodes/AnimatedAddition.js index 60713da500213c..f3d6b55167f404 100644 --- a/Libraries/Animated/src/nodes/AnimatedAddition.js +++ b/Libraries/Animated/src/nodes/AnimatedAddition.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/Animated/src/nodes/AnimatedDiffClamp.js b/Libraries/Animated/src/nodes/AnimatedDiffClamp.js index 4219eccfb377ff..3b657ef75ee23f 100644 --- a/Libraries/Animated/src/nodes/AnimatedDiffClamp.js +++ b/Libraries/Animated/src/nodes/AnimatedDiffClamp.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/Animated/src/nodes/AnimatedDivision.js b/Libraries/Animated/src/nodes/AnimatedDivision.js index 3498d322211f64..9efb4214dfa41f 100644 --- a/Libraries/Animated/src/nodes/AnimatedDivision.js +++ b/Libraries/Animated/src/nodes/AnimatedDivision.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/Animated/src/nodes/AnimatedInterpolation.js b/Libraries/Animated/src/nodes/AnimatedInterpolation.js index 8ee7cbba8a7fc0..1ba304d331ad5f 100644 --- a/Libraries/Animated/src/nodes/AnimatedInterpolation.js +++ b/Libraries/Animated/src/nodes/AnimatedInterpolation.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -14,7 +14,7 @@ const AnimatedNode = require('./AnimatedNode'); const AnimatedWithChildren = require('./AnimatedWithChildren'); const NativeAnimatedHelper = require('../NativeAnimatedHelper'); -const invariant = require('fbjs/lib/invariant'); +const invariant = require('invariant'); const normalizeColor = require('normalizeColor'); type ExtrapolateType = 'extend' | 'identity' | 'clamp'; @@ -347,24 +347,7 @@ class AnimatedInterpolation extends AnimatedWithChildren { } __transformDataType(range: Array) { - // Change the string array type to number array - // So we can reuse the same logic in iOS and Android platform - /* $FlowFixMe(>=0.70.0 site=react_native_fb) This comment suppresses an - * error found when Flow v0.70 was deployed. To see the error delete this - * comment and run Flow. */ - return range.map(function(value) { - if (typeof value !== 'string') { - return value; - } - if (/deg$/.test(value)) { - const degrees = parseFloat(value) || 0; - const radians = (degrees * Math.PI) / 180.0; - return radians; - } else { - // Assume radians - return parseFloat(value) || 0; - } - }); + return range.map(NativeAnimatedHelper.transformDataType); } __getNativeConfig(): any { diff --git a/Libraries/Animated/src/nodes/AnimatedModulo.js b/Libraries/Animated/src/nodes/AnimatedModulo.js index a3968566e9a2c4..a0cad545101ccb 100644 --- a/Libraries/Animated/src/nodes/AnimatedModulo.js +++ b/Libraries/Animated/src/nodes/AnimatedModulo.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/Animated/src/nodes/AnimatedMultiplication.js b/Libraries/Animated/src/nodes/AnimatedMultiplication.js index 889f530c76baee..b031ba613dc805 100644 --- a/Libraries/Animated/src/nodes/AnimatedMultiplication.js +++ b/Libraries/Animated/src/nodes/AnimatedMultiplication.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/Animated/src/nodes/AnimatedNode.js b/Libraries/Animated/src/nodes/AnimatedNode.js index 34e010a62f8c45..1d670f18a159c9 100644 --- a/Libraries/Animated/src/nodes/AnimatedNode.js +++ b/Libraries/Animated/src/nodes/AnimatedNode.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -11,7 +11,7 @@ const NativeAnimatedHelper = require('../NativeAnimatedHelper'); -const invariant = require('fbjs/lib/invariant'); +const invariant = require('invariant'); // Note(vjeux): this would be better as an interface but flow doesn't // support them yet diff --git a/Libraries/Animated/src/nodes/AnimatedProps.js b/Libraries/Animated/src/nodes/AnimatedProps.js index 75c9c25487135a..be3dfd86d5b4f2 100644 --- a/Libraries/Animated/src/nodes/AnimatedProps.js +++ b/Libraries/Animated/src/nodes/AnimatedProps.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -15,7 +15,7 @@ const AnimatedStyle = require('./AnimatedStyle'); const NativeAnimatedHelper = require('../NativeAnimatedHelper'); const ReactNative = require('ReactNative'); -const invariant = require('fbjs/lib/invariant'); +const invariant = require('invariant'); class AnimatedProps extends AnimatedNode { _props: Object; diff --git a/Libraries/Animated/src/nodes/AnimatedStyle.js b/Libraries/Animated/src/nodes/AnimatedStyle.js index 990b960a336805..e1f3d332a2280f 100644 --- a/Libraries/Animated/src/nodes/AnimatedStyle.js +++ b/Libraries/Animated/src/nodes/AnimatedStyle.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -95,13 +95,13 @@ class AnimatedStyle extends AnimatedWithChildren { } __makeNative() { - super.__makeNative(); for (const key in this._style) { const value = this._style[key]; if (value instanceof AnimatedNode) { value.__makeNative(); } } + super.__makeNative(); } __getNativeConfig(): Object { diff --git a/Libraries/Animated/src/nodes/AnimatedSubtraction.js b/Libraries/Animated/src/nodes/AnimatedSubtraction.js index 3fe8bbb6920703..c79860a4095074 100644 --- a/Libraries/Animated/src/nodes/AnimatedSubtraction.js +++ b/Libraries/Animated/src/nodes/AnimatedSubtraction.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/Animated/src/nodes/AnimatedTracking.js b/Libraries/Animated/src/nodes/AnimatedTracking.js index 699da167c7b2c7..284d5b39af7a0b 100644 --- a/Libraries/Animated/src/nodes/AnimatedTracking.js +++ b/Libraries/Animated/src/nodes/AnimatedTracking.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/Animated/src/nodes/AnimatedTransform.js b/Libraries/Animated/src/nodes/AnimatedTransform.js index 1706923394ec96..e3b3034675055c 100644 --- a/Libraries/Animated/src/nodes/AnimatedTransform.js +++ b/Libraries/Animated/src/nodes/AnimatedTransform.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -22,7 +22,6 @@ class AnimatedTransform extends AnimatedWithChildren { } __makeNative() { - super.__makeNative(); this._transforms.forEach(transform => { for (const key in transform) { const value = transform[key]; @@ -31,6 +30,7 @@ class AnimatedTransform extends AnimatedWithChildren { } } }); + super.__makeNative(); } __getValue(): $ReadOnlyArray { @@ -103,7 +103,7 @@ class AnimatedTransform extends AnimatedWithChildren { transConfigs.push({ type: 'static', property: key, - value, + value: NativeAnimatedHelper.transformDataType(value), }); } } diff --git a/Libraries/Animated/src/nodes/AnimatedValue.js b/Libraries/Animated/src/nodes/AnimatedValue.js index a3fb3a325b838e..e896f660000ab2 100644 --- a/Libraries/Animated/src/nodes/AnimatedValue.js +++ b/Libraries/Animated/src/nodes/AnimatedValue.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -10,7 +10,6 @@ 'use strict'; const AnimatedInterpolation = require('./AnimatedInterpolation'); -const AnimatedNode = require('./AnimatedNode'); const AnimatedWithChildren = require('./AnimatedWithChildren'); const InteractionManager = require('InteractionManager'); const NativeAnimatedHelper = require('../NativeAnimatedHelper'); diff --git a/Libraries/Animated/src/nodes/AnimatedValueXY.js b/Libraries/Animated/src/nodes/AnimatedValueXY.js index 3de5bbaf9d3a04..adc7a526a1eaa8 100644 --- a/Libraries/Animated/src/nodes/AnimatedValueXY.js +++ b/Libraries/Animated/src/nodes/AnimatedValueXY.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -12,7 +12,7 @@ const AnimatedValue = require('./AnimatedValue'); const AnimatedWithChildren = require('./AnimatedWithChildren'); -const invariant = require('fbjs/lib/invariant'); +const invariant = require('invariant'); type ValueXYListenerCallback = (value: {x: number, y: number}) => void; diff --git a/Libraries/Animated/src/nodes/AnimatedWithChildren.js b/Libraries/Animated/src/nodes/AnimatedWithChildren.js index 9cd5fb49e14070..3940c4ae206082 100644 --- a/Libraries/Animated/src/nodes/AnimatedWithChildren.js +++ b/Libraries/Animated/src/nodes/AnimatedWithChildren.js @@ -1,10 +1,10 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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 + * @flow strict-local * @format */ 'use strict'; diff --git a/Libraries/Animated/src/polyfills/InteractionManager.js b/Libraries/Animated/src/polyfills/InteractionManager.js index 5e82aaaa544cdb..c2247c762a3da0 100644 --- a/Libraries/Animated/src/polyfills/InteractionManager.js +++ b/Libraries/Animated/src/polyfills/InteractionManager.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/Animated/src/polyfills/Set.js b/Libraries/Animated/src/polyfills/Set.js index c88278bda1cb38..39205b8f9f9920 100644 --- a/Libraries/Animated/src/polyfills/Set.js +++ b/Libraries/Animated/src/polyfills/Set.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/Animated/src/polyfills/flattenStyle.js b/Libraries/Animated/src/polyfills/flattenStyle.js index 21907b235f97f5..b50057af28435f 100644 --- a/Libraries/Animated/src/polyfills/flattenStyle.js +++ b/Libraries/Animated/src/polyfills/flattenStyle.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/AppState/AppState.js b/Libraries/AppState/AppState.js index 554689f11ad680..95397a9c102f7f 100644 --- a/Libraries/AppState/AppState.js +++ b/Libraries/AppState/AppState.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -16,7 +16,7 @@ const NativeModules = require('NativeModules'); const RCTAppState = NativeModules.AppState; const logError = require('logError'); -const invariant = require('fbjs/lib/invariant'); +const invariant = require('invariant'); /** * `AppState` can tell you if the app is in the foreground or background, diff --git a/Libraries/BatchedBridge/BatchedBridge.js b/Libraries/BatchedBridge/BatchedBridge.js index 0b1aae766493e6..663b0051457e24 100644 --- a/Libraries/BatchedBridge/BatchedBridge.js +++ b/Libraries/BatchedBridge/BatchedBridge.js @@ -1,11 +1,11 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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 + * @flow strict-local */ 'use strict'; diff --git a/Libraries/BatchedBridge/MessageQueue.js b/Libraries/BatchedBridge/MessageQueue.js index c9b750df9efdc8..2b0d20fc2ff03b 100644 --- a/Libraries/BatchedBridge/MessageQueue.js +++ b/Libraries/BatchedBridge/MessageQueue.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -14,7 +14,7 @@ const ErrorUtils = require('ErrorUtils'); const Systrace = require('Systrace'); const deepFreezeAndThrowOnMutationInDev = require('deepFreezeAndThrowOnMutationInDev'); -const invariant = require('fbjs/lib/invariant'); +const invariant = require('invariant'); const stringifySafe = require('stringifySafe'); export type SpyData = { @@ -60,7 +60,7 @@ class MessageQueue { this._failureCallbacks = {}; this._callID = 0; this._lastFlush = 0; - this._eventLoopStartTime = new Date().getTime(); + this._eventLoopStartTime = Date.now(); this._immediatesCallback = null; if (__DEV__) { @@ -141,7 +141,7 @@ class MessageQueue { } getEventLoopRunningTime() { - return new Date().getTime() - this._eventLoopStartTime; + return Date.now() - this._eventLoopStartTime; } registerCallableModule(name: string, module: Object) { @@ -213,11 +213,13 @@ class MessageQueue { t === 'undefined' || t === 'null' || t === 'boolean' || - t === 'number' || t === 'string' ) { return true; } + if (t === 'number') { + return isFinite(val); + } if (t === 'function' || t !== 'object') { return false; } @@ -232,10 +234,25 @@ class MessageQueue { return true; }; + // Replacement allows normally non-JSON-convertible values to be + // seen. There is ambiguity with string values, but in context, + // it should at least be a strong hint. + const replacer = (key, val) => { + const t = typeof val; + if (t === 'function') { + return '<>'; + } else if (t === 'number' && !isFinite(val)) { + return '<<' + val.toString() + '>>'; + } else { + return val; + } + }; + + // Note that JSON.stringify invariant( isValidArgument(params), '%s is not usable as a native method argument', - params, + JSON.stringify(params, replacer), ); // The params object should not be mutated after being queued @@ -243,7 +260,7 @@ class MessageQueue { } this._queue[PARAMS].push(params); - const now = new Date().getTime(); + const now = Date.now(); if ( global.nativeFlushQueueImmediate && now - this._lastFlush >= MIN_TIME_BETWEEN_FLUSHES_MS @@ -323,7 +340,7 @@ class MessageQueue { } __callFunction(module: string, method: string, args: any[]): any { - this._lastFlush = new Date().getTime(); + this._lastFlush = Date.now(); this._eventLoopStartTime = this._lastFlush; if (__DEV__ || this.__spy) { Systrace.beginEvent(`${module}.${method}(${stringifySafe(args)})`); @@ -352,7 +369,7 @@ class MessageQueue { } __invokeCallback(cbID: number, args: any[]) { - this._lastFlush = new Date().getTime(); + this._lastFlush = Date.now(); this._eventLoopStartTime = this._lastFlush; // The rightmost bit of cbID indicates fail (0) or success (1), the other bits are the callID shifted left. diff --git a/Libraries/BatchedBridge/NativeModules.js b/Libraries/BatchedBridge/NativeModules.js index 61dd5bff1e61df..27db8b9ad3e3c3 100644 --- a/Libraries/BatchedBridge/NativeModules.js +++ b/Libraries/BatchedBridge/NativeModules.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -12,7 +12,7 @@ const BatchedBridge = require('BatchedBridge'); -const invariant = require('fbjs/lib/invariant'); +const invariant = require('invariant'); import type {ExtendedError} from 'parseErrorStack'; @@ -60,8 +60,17 @@ function genModule( const methodType = isPromise ? 'promise' : isSync ? 'sync' : 'async'; module[methodName] = genMethod(moduleID, methodID, methodType); }); + Object.assign(module, constants); + if (module.getConstants == null) { + module.getConstants = () => constants; + } else { + console.warn( + `Unable to define method 'getConstants()' on NativeModule '${moduleName}'. NativeModule '${moduleName}' already has a constant or method called 'getConstants'. Please remove it.`, + ); + } + if (__DEV__) { BatchedBridge.createDebugLookup(moduleID, moduleName, methods); } diff --git a/Libraries/BatchedBridge/__mocks__/MessageQueueTestConfig.js b/Libraries/BatchedBridge/__mocks__/MessageQueueTestConfig.js index 3f9ae5f0a4855c..bb50f5d2fa7b50 100644 --- a/Libraries/BatchedBridge/__mocks__/MessageQueueTestConfig.js +++ b/Libraries/BatchedBridge/__mocks__/MessageQueueTestConfig.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2013-present, Facebook, Inc. + * 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. diff --git a/Libraries/BatchedBridge/__mocks__/MessageQueueTestModule.js b/Libraries/BatchedBridge/__mocks__/MessageQueueTestModule.js index 05168a6b570094..37425cd7652706 100644 --- a/Libraries/BatchedBridge/__mocks__/MessageQueueTestModule.js +++ b/Libraries/BatchedBridge/__mocks__/MessageQueueTestModule.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2013-present, Facebook, Inc. + * 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. diff --git a/Libraries/BatchedBridge/__tests__/MessageQueue-test.js b/Libraries/BatchedBridge/__tests__/MessageQueue-test.js index 555a9e2053be33..77cd6948124487 100644 --- a/Libraries/BatchedBridge/__tests__/MessageQueue-test.js +++ b/Libraries/BatchedBridge/__tests__/MessageQueue-test.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2013-present, Facebook, Inc. + * 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. diff --git a/Libraries/BatchedBridge/__tests__/NativeModules-test.js b/Libraries/BatchedBridge/__tests__/NativeModules-test.js index 9702146b719090..821c3261a4169a 100644 --- a/Libraries/BatchedBridge/__tests__/NativeModules-test.js +++ b/Libraries/BatchedBridge/__tests__/NativeModules-test.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2013-present, Facebook, Inc. + * 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. @@ -10,12 +10,7 @@ 'use strict'; -jest - .enableAutomock() - .unmock('BatchedBridge') - .unmock('defineLazyObjectProperty') - .unmock('MessageQueue') - .unmock('NativeModules'); +jest.unmock('NativeModules'); let BatchedBridge; let NativeModules; diff --git a/Libraries/Blob/Blob.js b/Libraries/Blob/Blob.js index 0e36b07f7f4e5a..c7e013dfa0b530 100644 --- a/Libraries/Blob/Blob.js +++ b/Libraries/Blob/Blob.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2013-present, Facebook, Inc. + * 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. diff --git a/Libraries/Blob/BlobManager.js b/Libraries/Blob/BlobManager.js index b08082e35e1580..460980bb5b21ef 100644 --- a/Libraries/Blob/BlobManager.js +++ b/Libraries/Blob/BlobManager.js @@ -1,10 +1,10 @@ /** - * Copyright (c) 2013-present, Facebook, Inc. + * 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 + * @flow strict-local * @format */ diff --git a/Libraries/Blob/BlobRegistry.js b/Libraries/Blob/BlobRegistry.js index 445fe6bb855835..cfd0475a53ea07 100644 --- a/Libraries/Blob/BlobRegistry.js +++ b/Libraries/Blob/BlobRegistry.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2013-present, Facebook, Inc. + * 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. diff --git a/Libraries/Blob/BlobTypes.js b/Libraries/Blob/BlobTypes.js index 2e5be53e19615f..de105d2a6904d3 100644 --- a/Libraries/Blob/BlobTypes.js +++ b/Libraries/Blob/BlobTypes.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2013-present, Facebook, Inc. + * 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. diff --git a/Libraries/Blob/File.js b/Libraries/Blob/File.js index 2f57183cdb3f19..2510d32b204c60 100644 --- a/Libraries/Blob/File.js +++ b/Libraries/Blob/File.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2013-present, Facebook, Inc. + * 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. @@ -11,7 +11,7 @@ const Blob = require('Blob'); -const invariant = require('fbjs/lib/invariant'); +const invariant = require('invariant'); import type {BlobOptions} from 'BlobTypes'; diff --git a/Libraries/Blob/FileReader.js b/Libraries/Blob/FileReader.js index 309e4577a455f8..b9181ed9aaa34b 100644 --- a/Libraries/Blob/FileReader.js +++ b/Libraries/Blob/FileReader.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/Blob/RCTBlobManager.h b/Libraries/Blob/RCTBlobManager.h index 24b589ef987ae5..1d5750799e3d29 100755 --- a/Libraries/Blob/RCTBlobManager.h +++ b/Libraries/Blob/RCTBlobManager.h @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/Blob/RCTBlobManager.mm b/Libraries/Blob/RCTBlobManager.mm index 7c0cbb8673f064..597d9b23d4edbb 100755 --- a/Libraries/Blob/RCTBlobManager.mm +++ b/Libraries/Blob/RCTBlobManager.mm @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -48,6 +48,11 @@ + (BOOL)requiresMainQueueSetup } - (NSDictionary *)constantsToExport +{ + return [self getConstants]; +} + +- (NSDictionary *)getConstants { return @{ @"BLOB_URI_SCHEME": kBlobURIScheme, diff --git a/Libraries/Blob/RCTFileReaderModule.h b/Libraries/Blob/RCTFileReaderModule.h index 72d224b0d968cb..89c018381bcd32 100644 --- a/Libraries/Blob/RCTFileReaderModule.h +++ b/Libraries/Blob/RCTFileReaderModule.h @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/Blob/RCTFileReaderModule.m b/Libraries/Blob/RCTFileReaderModule.m index 5059e0b84180ee..26b33f63e471a9 100644 --- a/Libraries/Blob/RCTFileReaderModule.m +++ b/Libraries/Blob/RCTFileReaderModule.m @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/Blob/URL.js b/Libraries/Blob/URL.js index de73affac2af1b..049a91ec03026a 100644 --- a/Libraries/Blob/URL.js +++ b/Libraries/Blob/URL.js @@ -1,11 +1,10 @@ /** - * Copyright (c) 2013-present, Facebook, Inc. + * 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'; @@ -47,11 +46,71 @@ if (BlobModule && typeof BlobModule.BLOB_URI_SCHEME === 'string') { * * ``` */ -class URL { - constructor() { - throw new Error('Creating URL objects is not supported yet.'); + +// Small subset from whatwg-url: https://github.com/jsdom/whatwg-url/tree/master/lib +// The reference code bloat comes from Unicode issues with URLs, so those won't work here. +export class URLSearchParams { + _searchParams = []; + + constructor(params: any) { + if (typeof params === 'object') { + Object.keys(params).forEach(key => this.append(key, params[key])); + } + } + + append(key: string, value: string) { + this._searchParams.push([key, value]); + } + + delete(name) { + throw new Error('not implemented'); + } + + get(name) { + throw new Error('not implemented'); + } + + getAll(name) { + throw new Error('not implemented'); + } + + has(name) { + throw new Error('not implemented'); + } + + set(name, value) { + throw new Error('not implemented'); } + sort() { + throw new Error('not implemented'); + } + + [Symbol.iterator]() { + return this._searchParams[Symbol.iterator](); + } + + toString() { + if (this._searchParams.length === 0) { + return ''; + } + const last = this._searchParams.length - 1; + return this._searchParams.reduce((acc, curr, index) => { + return acc + curr.join('=') + (index === last ? '' : '&'); + }, ''); + } +} + +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( + url, + ); +} + +export class URL { + _searchParamsInstance = null; + static createObjectURL(blob: Blob) { if (BLOB_URL_PREFIX === null) { throw new Error('Cannot create URL for blob!'); @@ -64,6 +123,93 @@ class URL { static revokeObjectURL(url: string) { // Do nothing. } -} -module.exports = URL; + constructor(url: string, base: string) { + let baseUrl = null; + if (base) { + if (typeof base === 'string') { + baseUrl = base; + if (!validateBaseUrl(baseUrl)) { + throw new TypeError(`Invalid base URL: ${baseUrl}`); + } + } else if (typeof base === 'object') { + baseUrl = base.toString(); + } + if (baseUrl.endsWith('/') && url.startsWith('/')) { + baseUrl = baseUrl.slice(0, baseUrl.length - 1); + } + if (baseUrl.endsWith(url)) { + url = ''; + } + this._url = `${baseUrl}${url}`; + } else { + this._url = url; + if (!this._url.endsWith('/')) { + this._url += '/'; + } + } + } + + get hash() { + throw new Error('not implemented'); + } + + get host() { + throw new Error('not implemented'); + } + + get hostname() { + throw new Error('not implemented'); + } + + get href(): string { + return this.toString(); + } + + get origin() { + throw new Error('not implemented'); + } + + get password() { + throw new Error('not implemented'); + } + + get pathname() { + throw new Error('not implemented'); + } + + get port() { + throw new Error('not implemented'); + } + + get protocol() { + throw new Error('not implemented'); + } + + get search() { + throw new Error('not implemented'); + } + + get searchParams(): URLSearchParams { + if (this._searchParamsInstance == null) { + this._searchParamsInstance = new URLSearchParams(); + } + return this._searchParamsInstance; + } + + toJSON(): string { + return this.toString(); + } + + toString(): string { + if (this._searchParamsInstance === null) { + return this._url; + } + const separator = this._url.indexOf('?') > -1 ? '&' : '?'; + return this._url + separator + this._searchParamsInstance.toString(); + } + + get username() { + throw new Error('not implemented'); + } +} diff --git a/Libraries/Blob/__mocks__/BlobModule.js b/Libraries/Blob/__mocks__/BlobModule.js index e904d2929dd74b..3a1cb8eda6174b 100644 --- a/Libraries/Blob/__mocks__/BlobModule.js +++ b/Libraries/Blob/__mocks__/BlobModule.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/Blob/__mocks__/FileReaderModule.js b/Libraries/Blob/__mocks__/FileReaderModule.js index 0f35aa89ace07e..da687eca470b1d 100644 --- a/Libraries/Blob/__mocks__/FileReaderModule.js +++ b/Libraries/Blob/__mocks__/FileReaderModule.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/Blob/__tests__/Blob-test.js b/Libraries/Blob/__tests__/Blob-test.js index 47bf5fee7c1361..7600c0c967825c 100644 --- a/Libraries/Blob/__tests__/Blob-test.js +++ b/Libraries/Blob/__tests__/Blob-test.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/Blob/__tests__/BlobManager-test.js b/Libraries/Blob/__tests__/BlobManager-test.js index c141d42f16334c..73ad2a9de1103f 100644 --- a/Libraries/Blob/__tests__/BlobManager-test.js +++ b/Libraries/Blob/__tests__/BlobManager-test.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/Blob/__tests__/File-test.js b/Libraries/Blob/__tests__/File-test.js index c10f90c18766ae..11b25116809a5d 100644 --- a/Libraries/Blob/__tests__/File-test.js +++ b/Libraries/Blob/__tests__/File-test.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/Blob/__tests__/FileReader-test.js b/Libraries/Blob/__tests__/FileReader-test.js index b2237550bf7184..a375c3ef374710 100644 --- a/Libraries/Blob/__tests__/FileReader-test.js +++ b/Libraries/Blob/__tests__/FileReader-test.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/Blob/__tests__/URL-test.js b/Libraries/Blob/__tests__/URL-test.js new file mode 100644 index 00000000000000..051ec9a724e164 --- /dev/null +++ b/Libraries/Blob/__tests__/URL-test.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 + * @emails oncall+react_native + */ +'use strict'; + +const URL = require('URL').URL; + +describe('URL', function() { + it('should pass Mozilla Dev Network examples', () => { + const a = new URL('/', 'https://developer.mozilla.org'); + expect(a.href).toBe('https://developer.mozilla.org/'); + const b = new URL('https://developer.mozilla.org'); + expect(b.href).toBe('https://developer.mozilla.org/'); + const c = new URL('en-US/docs', b); + expect(c.href).toBe('https://developer.mozilla.org/en-US/docs'); + const d = new URL('/en-US/docs', b); + expect(d.href).toBe('https://developer.mozilla.org/en-US/docs'); + const f = new URL('/en-US/docs', d); + expect(f.href).toBe('https://developer.mozilla.org/en-US/docs'); + // from original test suite, but requires complex implementation + // const g = new URL( + // '/en-US/docs', + // 'https://developer.mozilla.org/fr-FR/toto', + // ); + // expect(g.href).toBe('https://developer.mozilla.org/en-US/docs'); + const h = new URL('/en-US/docs', a); + expect(h.href).toBe('https://developer.mozilla.org/en-US/docs'); + }); +}); diff --git a/Libraries/BugReporting/BugReporting.js b/Libraries/BugReporting/BugReporting.js index 6c63b6a1eebf98..1623b71741f059 100644 --- a/Libraries/BugReporting/BugReporting.js +++ b/Libraries/BugReporting/BugReporting.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2013-present, Facebook, Inc. + * 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. @@ -11,7 +11,6 @@ 'use strict'; const RCTDeviceEventEmitter = require('RCTDeviceEventEmitter'); -const Map = require('Map'); const infoLog = require('infoLog'); import type EmitterSubscription from 'EmitterSubscription'; diff --git a/Libraries/BugReporting/dumpReactTree.js b/Libraries/BugReporting/dumpReactTree.js index ead8f4bdd76c32..2ef17ecf034784 100644 --- a/Libraries/BugReporting/dumpReactTree.js +++ b/Libraries/BugReporting/dumpReactTree.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2013-present, Facebook, Inc. + * 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. diff --git a/Libraries/BugReporting/getReactData.js b/Libraries/BugReporting/getReactData.js index b4d1c4a32fd557..98221590c37072 100644 --- a/Libraries/BugReporting/getReactData.js +++ b/Libraries/BugReporting/getReactData.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -142,8 +142,14 @@ function setInContext(inst, path: Array, value: any) { function setIn(obj: Object, path: Array, value: any) { const last = path.pop(); + /* $FlowFixMe(>=0.88.0 site=react_native_fb) This comment suppresses an error + * found when Flow v0.88 was deployed. To see the error, delete this comment + * and run Flow. */ const parent = path.reduce((obj_, attr) => (obj_ ? obj_[attr] : null), obj); if (parent) { + /* $FlowFixMe(>=0.88.0 site=react_native_fb) This comment suppresses an + * error found when Flow v0.88 was deployed. To see the error, delete this + * comment and run Flow. */ parent[last] = value; } } diff --git a/Libraries/CameraRoll/CameraRoll.js b/Libraries/CameraRoll/CameraRoll.js index f2a4f65aa80933..68a06805d2ab75 100644 --- a/Libraries/CameraRoll/CameraRoll.js +++ b/Libraries/CameraRoll/CameraRoll.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -13,8 +13,8 @@ const PropTypes = require('prop-types'); const {checkPropTypes} = PropTypes; const RCTCameraRollManager = require('NativeModules').CameraRollManager; -const createStrictShapeTypeChecker = require('createStrictShapeTypeChecker'); -const invariant = require('fbjs/lib/invariant'); +const deprecatedCreateStrictShapeTypeChecker = require('deprecatedCreateStrictShapeTypeChecker'); +const invariant = require('invariant'); const GROUP_TYPES_OPTIONS = { Album: 'Album', @@ -32,10 +32,12 @@ const ASSET_TYPE_OPTIONS = { Photos: 'Photos', }; -type GetPhotosParams = { +export type GroupTypes = $Keys; + +export type GetPhotosParams = { first: number, after?: string, - groupTypes?: $Keys, + groupTypes?: GroupTypes, groupName?: string, assetType?: $Keys, mimeTypes?: Array, @@ -44,7 +46,7 @@ type GetPhotosParams = { /** * Shape of the param arg for the `getPhotos` function. */ -const getPhotosParamChecker = createStrictShapeTypeChecker({ +const getPhotosParamChecker = deprecatedCreateStrictShapeTypeChecker({ /** * The number of photos wanted in reverse order of the photo application * (i.e. most recent first for SavedPhotos). @@ -79,48 +81,51 @@ const getPhotosParamChecker = createStrictShapeTypeChecker({ mimeTypes: PropTypes.arrayOf(PropTypes.string), }); -type GetPhotosReturn = Promise<{ - edges: Array<{ - node: { - type: string, - group_name: string, - image: { - uri: string, - height: number, - width: number, - isStored?: boolean, - playableDuration: number, - }, - timestamp: number, - location?: { - latitude?: number, - longitude?: number, - altitude?: number, - heading?: number, - speed?: number, - }, +export type PhotoIdentifier = { + node: { + type: string, + group_name: string, + image: { + filename: string, + uri: string, + height: number, + width: number, + isStored?: boolean, + playableDuration: number, + }, + timestamp: number, + location?: { + latitude?: number, + longitude?: number, + altitude?: number, + heading?: number, + speed?: number, }, - }>, + }, +}; + +export type PhotoIdentifiersPage = { + edges: Array, page_info: { has_next_page: boolean, start_cursor?: string, end_cursor?: string, }, -}>; +}; /** * Shape of the return value of the `getPhotos` function. */ -const getPhotosReturnChecker = createStrictShapeTypeChecker({ +const getPhotosReturnChecker = deprecatedCreateStrictShapeTypeChecker({ edges: PropTypes.arrayOf( /* $FlowFixMe(>=0.66.0 site=react_native_fb) This comment suppresses an * error found when Flow v0.66 was deployed. To see the error delete this * comment and run Flow. */ - createStrictShapeTypeChecker({ - node: createStrictShapeTypeChecker({ + deprecatedCreateStrictShapeTypeChecker({ + node: deprecatedCreateStrictShapeTypeChecker({ type: PropTypes.string.isRequired, group_name: PropTypes.string.isRequired, - image: createStrictShapeTypeChecker({ + image: deprecatedCreateStrictShapeTypeChecker({ uri: PropTypes.string.isRequired, height: PropTypes.number.isRequired, width: PropTypes.number.isRequired, @@ -128,7 +133,7 @@ const getPhotosReturnChecker = createStrictShapeTypeChecker({ playableDuration: PropTypes.number.isRequired, }).isRequired, timestamp: PropTypes.number.isRequired, - location: createStrictShapeTypeChecker({ + location: deprecatedCreateStrictShapeTypeChecker({ latitude: PropTypes.number, longitude: PropTypes.number, altitude: PropTypes.number, @@ -138,7 +143,7 @@ const getPhotosReturnChecker = createStrictShapeTypeChecker({ }).isRequired, }), ).isRequired, - page_info: createStrictShapeTypeChecker({ + page_info: deprecatedCreateStrictShapeTypeChecker({ has_next_page: PropTypes.bool.isRequired, start_cursor: PropTypes.string, end_cursor: PropTypes.string, @@ -151,8 +156,8 @@ const getPhotosReturnChecker = createStrictShapeTypeChecker({ * See https://facebook.github.io/react-native/docs/cameraroll.html */ class CameraRoll { - static GroupTypesOptions: Object = GROUP_TYPES_OPTIONS; - static AssetTypeOptions: Object = ASSET_TYPE_OPTIONS; + static GroupTypesOptions = GROUP_TYPES_OPTIONS; + static AssetTypeOptions = ASSET_TYPE_OPTIONS; /** * `CameraRoll.saveImageWithTag()` is deprecated. Use `CameraRoll.saveToCameraRoll()` instead. @@ -204,7 +209,7 @@ class CameraRoll { * * See https://facebook.github.io/react-native/docs/cameraroll.html#getphotos */ - static getPhotos(params: GetPhotosParams): GetPhotosReturn { + static getPhotos(params: GetPhotosParams): Promise { if (__DEV__) { checkPropTypes( {params: getPhotosParamChecker}, diff --git a/Libraries/CameraRoll/ImagePickerIOS.js b/Libraries/CameraRoll/ImagePickerIOS.js index 785f4b57cb6b56..9c783339835f88 100644 --- a/Libraries/CameraRoll/ImagePickerIOS.js +++ b/Libraries/CameraRoll/ImagePickerIOS.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/CameraRoll/RCTAssetsLibraryRequestHandler.h b/Libraries/CameraRoll/RCTAssetsLibraryRequestHandler.h index 8189bb56e4ea8c..7a0f0635ee7b75 100644 --- a/Libraries/CameraRoll/RCTAssetsLibraryRequestHandler.h +++ b/Libraries/CameraRoll/RCTAssetsLibraryRequestHandler.h @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -8,17 +8,8 @@ #import #import -@class ALAssetsLibrary; +@class PHPhotoLibrary; @interface RCTAssetsLibraryRequestHandler : NSObject @end - -@interface RCTBridge (RCTAssetsLibraryImageLoader) - -/** - * The shared asset library instance. - */ -@property (nonatomic, readonly) ALAssetsLibrary *assetsLibrary; - -@end diff --git a/Libraries/CameraRoll/RCTAssetsLibraryRequestHandler.m b/Libraries/CameraRoll/RCTAssetsLibraryRequestHandler.m index 9721b392b80f78..cdc6cb9f203ebc 100644 --- a/Libraries/CameraRoll/RCTAssetsLibraryRequestHandler.m +++ b/Libraries/CameraRoll/RCTAssetsLibraryRequestHandler.m @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -11,41 +11,26 @@ #import #import -#import +#import #import #import #import @implementation RCTAssetsLibraryRequestHandler -{ - ALAssetsLibrary *_assetsLibrary; -} RCT_EXPORT_MODULE() -@synthesize bridge = _bridge; -static Class _ALAssetsLibrary = nil; -static void ensureAssetsLibLoaded(void) -{ - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - void * handle = dlopen("/System/Library/Frameworks/AssetsLibrary.framework/AssetsLibrary", RTLD_LAZY); -#pragma unused(handle) - _ALAssetsLibrary = objc_getClass("ALAssetsLibrary"); - }); -} -- (ALAssetsLibrary *)assetsLibrary -{ - ensureAssetsLibLoaded(); - return _assetsLibrary ?: (_assetsLibrary = [_ALAssetsLibrary new]); -} - #pragma mark - RCTURLRequestHandler - (BOOL)canHandleRequest:(NSURLRequest *)request { - return [request.URL.scheme caseInsensitiveCompare:@"assets-library"] == NSOrderedSame; + if (![PHAsset class]) { + return NO; + } + + return [request.URL.scheme caseInsensitiveCompare:@"assets-library"] == NSOrderedSame + || [request.URL.scheme caseInsensitiveCompare:@"ph"] == NSOrderedSame; } - (id)sendRequest:(NSURLRequest *)request @@ -55,58 +40,75 @@ - (id)sendRequest:(NSURLRequest *)request void (^cancellationBlock)(void) = ^{ atomic_store(&cancelled, YES); }; - - [[self assetsLibrary] assetForURL:request.URL resultBlock:^(ALAsset *asset) { - if (atomic_load(&cancelled)) { - return; - } - - if (asset) { - - ALAssetRepresentation *representation = [asset defaultRepresentation]; - NSInteger length = (NSInteger)representation.size; - CFStringRef MIMEType = UTTypeCopyPreferredTagWithClass((__bridge CFStringRef _Nonnull)(representation.UTI), kUTTagClassMIMEType); - - NSURLResponse *response = - [[NSURLResponse alloc] initWithURL:request.URL - MIMEType:(__bridge NSString *)(MIMEType) - expectedContentLength:length - textEncodingName:nil]; - - [delegate URLRequest:cancellationBlock didReceiveResponse:response]; - - NSError *error = nil; - uint8_t *buffer = (uint8_t *)malloc((size_t)length); - if ([representation getBytes:buffer - fromOffset:0 - length:length - error:&error]) { - - NSData *data = [[NSData alloc] initWithBytesNoCopy:buffer - length:length - freeWhenDone:YES]; - - [delegate URLRequest:cancellationBlock didReceiveData:data]; - [delegate URLRequest:cancellationBlock didCompleteWithError:nil]; - - } else { - free(buffer); - [delegate URLRequest:cancellationBlock didCompleteWithError:error]; - } - - } else { - NSString *errorMessage = [NSString stringWithFormat:@"Failed to load asset" - " at URL %@ with no error message.", request.URL]; - NSError *error = RCTErrorWithMessage(errorMessage); + + if (!request.URL) { + NSString *const msg = [NSString stringWithFormat:@"Cannot send request without URL"]; + [delegate URLRequest:cancellationBlock didCompleteWithError:RCTErrorWithMessage(msg)]; + return cancellationBlock; + } + + PHFetchResult *fetchResult; + + if ([request.URL.scheme caseInsensitiveCompare:@"ph"] == NSOrderedSame) { + // Fetch assets using PHAsset localIdentifier (recommended) + NSString *const localIdentifier = [request.URL.absoluteString substringFromIndex:@"ph://".length]; + fetchResult = [PHAsset fetchAssetsWithLocalIdentifiers:@[localIdentifier] options:nil]; + } else if ([request.URL.scheme caseInsensitiveCompare:@"assets-library"] == NSOrderedSame) { + // This is the older, deprecated way of fetching assets from assets-library + // using the "assets-library://" protocol + fetchResult = [PHAsset fetchAssetsWithALAssetURLs:@[request.URL] options:nil]; + } else { + NSString *const msg = [NSString stringWithFormat:@"Cannot send request with unknown protocol: %@", request.URL]; + [delegate URLRequest:cancellationBlock didCompleteWithError:RCTErrorWithMessage(msg)]; + return cancellationBlock; + } + + if (![fetchResult firstObject]) { + NSString *errorMessage = [NSString stringWithFormat:@"Failed to load asset" + " at URL %@ with no error message.", request.URL]; + NSError *error = RCTErrorWithMessage(errorMessage); + [delegate URLRequest:cancellationBlock didCompleteWithError:error]; + return cancellationBlock; + } + + if (atomic_load(&cancelled)) { + return cancellationBlock; + } + + PHAsset *const _Nonnull asset = [fetchResult firstObject]; + + // By default, allow downloading images from iCloud + PHImageRequestOptions *const requestOptions = [PHImageRequestOptions new]; + requestOptions.networkAccessAllowed = YES; + + [[PHImageManager defaultManager] requestImageDataForAsset:asset + options:requestOptions + resultHandler:^(NSData * _Nullable imageData, + NSString * _Nullable dataUTI, + UIImageOrientation orientation, + NSDictionary * _Nullable info) { + NSError *const error = [info objectForKey:PHImageErrorKey]; + if (error) { [delegate URLRequest:cancellationBlock didCompleteWithError:error]; - } - } failureBlock:^(NSError *loadError) { - if (atomic_load(&cancelled)) { return; } - [delegate URLRequest:cancellationBlock didCompleteWithError:loadError]; - }]; + NSInteger const length = [imageData length]; + CFStringRef const dataUTIStringRef = (__bridge CFStringRef _Nonnull)(dataUTI); + CFStringRef const mimeType = UTTypeCopyPreferredTagWithClass(dataUTIStringRef, kUTTagClassMIMEType); + + NSURLResponse *const response = [[NSURLResponse alloc] initWithURL:request.URL + MIMEType:(__bridge NSString *)(mimeType) + expectedContentLength:length + textEncodingName:nil]; + CFRelease(mimeType); + + [delegate URLRequest:cancellationBlock didReceiveResponse:response]; + + [delegate URLRequest:cancellationBlock didReceiveData:imageData]; + [delegate URLRequest:cancellationBlock didCompleteWithError:nil]; + }]; + return cancellationBlock; } @@ -116,12 +118,3 @@ - (void)cancelRequest:(id)requestToken } @end - -@implementation RCTBridge (RCTAssetsLibraryImageLoader) - -- (ALAssetsLibrary *)assetsLibrary -{ - return [[self moduleForClass:[RCTAssetsLibraryRequestHandler class]] assetsLibrary]; -} - -@end diff --git a/Libraries/CameraRoll/RCTCameraRollManager.h b/Libraries/CameraRoll/RCTCameraRollManager.h index b8b2c7d2ad63c7..b5d25a04d1eebd 100644 --- a/Libraries/CameraRoll/RCTCameraRollManager.h +++ b/Libraries/CameraRoll/RCTCameraRollManager.h @@ -1,22 +1,22 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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 +#import #import #import -@interface RCTConvert (ALAssetGroup) +@interface RCTConvert (PHFetchOptions) -+ (ALAssetsGroupType)ALAssetsGroupType:(id)json; -+ (ALAssetsFilter *)ALAssetsFilter:(id)json; ++ (PHFetchOptions *)PHFetchOptionsFromMediaType:(NSString *)mediaType; @end + @interface RCTCameraRollManager : NSObject @end diff --git a/Libraries/CameraRoll/RCTCameraRollManager.m b/Libraries/CameraRoll/RCTCameraRollManager.m index e6e397392bd159..245546a8f12564 100644 --- a/Libraries/CameraRoll/RCTCameraRollManager.m +++ b/Libraries/CameraRoll/RCTCameraRollManager.m @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -13,6 +13,7 @@ #import #import #import +#import #import #import @@ -22,85 +23,46 @@ #import "RCTAssetsLibraryRequestHandler.h" -@implementation RCTConvert (ALAssetGroup) - -RCT_ENUM_CONVERTER(ALAssetsGroupType, (@{ - - // New values - @"album": @(ALAssetsGroupAlbum), - @"all": @(ALAssetsGroupAll), - @"event": @(ALAssetsGroupEvent), - @"faces": @(ALAssetsGroupFaces), - @"library": @(ALAssetsGroupLibrary), - @"photo-stream": @(ALAssetsGroupPhotoStream), - @"saved-photos": @(ALAssetsGroupSavedPhotos), - - // Legacy values - @"Album": @(ALAssetsGroupAlbum), - @"All": @(ALAssetsGroupAll), - @"Event": @(ALAssetsGroupEvent), - @"Faces": @(ALAssetsGroupFaces), - @"Library": @(ALAssetsGroupLibrary), - @"PhotoStream": @(ALAssetsGroupPhotoStream), - @"SavedPhotos": @(ALAssetsGroupSavedPhotos), - -}), ALAssetsGroupSavedPhotos, integerValue) - -static Class _ALAssetsFilter = nil; -static NSString *_ALAssetsGroupPropertyName = nil; -static NSString *_ALAssetPropertyAssetURL = nil; -static NSString *_ALAssetPropertyLocation = nil; -static NSString *_ALAssetPropertyDate = nil; -static NSString *_ALAssetPropertyType = nil; -static NSString *_ALAssetPropertyDuration = nil; -static NSString *_ALAssetTypeVideo = nil; -static NSString *lookupNSString(void * handle, const char * name) -{ - void ** sym = dlsym(handle, name); - return (__bridge NSString *)(sym ? *sym : nil); -} -static void ensureAssetsLibLoaded(void) -{ - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - void * handle = dlopen("/System/Library/Frameworks/AssetsLibrary.framework/AssetsLibrary", RTLD_LAZY); - RCTAssert(handle != NULL, @"Unable to load AssetsLibrary.framework."); - _ALAssetsFilter = objc_getClass("ALAssetsFilter"); - _ALAssetsGroupPropertyName = lookupNSString(handle, "ALAssetsGroupPropertyName"); - _ALAssetPropertyAssetURL = lookupNSString(handle, "ALAssetPropertyAssetURL"); - _ALAssetPropertyLocation = lookupNSString(handle, "ALAssetPropertyLocation"); - _ALAssetPropertyDate = lookupNSString(handle, "ALAssetPropertyDate"); - _ALAssetPropertyType = lookupNSString(handle, "ALAssetPropertyType"); - _ALAssetPropertyDuration = lookupNSString(handle, "ALAssetPropertyDuration"); - _ALAssetTypeVideo = lookupNSString(handle, "ALAssetTypeVideo"); - }); -} +@implementation RCTConvert (PHAssetCollectionSubtype) + +RCT_ENUM_CONVERTER(PHAssetCollectionSubtype, (@{ + @"album": @(PHAssetCollectionSubtypeAny), + @"all": @(PHAssetCollectionSubtypeAny), + @"event": @(PHAssetCollectionSubtypeAlbumSyncedEvent), + @"faces": @(PHAssetCollectionSubtypeAlbumSyncedFaces), + @"library": @(PHAssetCollectionSubtypeSmartAlbumUserLibrary), + @"photo-stream": @(PHAssetCollectionSubtypeAlbumMyPhotoStream), // incorrect, but legacy + @"photostream": @(PHAssetCollectionSubtypeAlbumMyPhotoStream), + @"saved-photos": @(PHAssetCollectionSubtypeAny), // incorrect, but legacy + @"savedphotos": @(PHAssetCollectionSubtypeAny), // This was ALAssetsGroupSavedPhotos, seems to have no direct correspondence in PHAssetCollectionSubtype +}), PHAssetCollectionSubtypeAny, integerValue) + -+ (ALAssetsFilter *)ALAssetsFilter:(id)json +@end + +@implementation RCTConvert (PHFetchOptions) + ++ (PHFetchOptions *)PHFetchOptionsFromMediaType:(NSString *)mediaType { - static NSDictionary *options; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - ensureAssetsLibLoaded(); - options = @{ - // New values - @"photos": [_ALAssetsFilter allPhotos], - @"videos": [_ALAssetsFilter allVideos], - @"all": [_ALAssetsFilter allAssets], - - // Legacy values - @"Photos": [_ALAssetsFilter allPhotos], - @"Videos": [_ALAssetsFilter allVideos], - @"All": [_ALAssetsFilter allAssets], - }; - }); + // This is not exhaustive in terms of supported media type predicates; more can be added in the future + NSString *const lowercase = [mediaType lowercaseString]; - ALAssetsFilter *filter = options[json ?: @"photos"]; - if (!filter) { - RCTLogError(@"Invalid filter option: '%@'. Expected one of 'photos'," - "'videos' or 'all'.", json); + if ([lowercase isEqualToString:@"photos"]) { + PHFetchOptions *const options = [PHFetchOptions new]; + options.predicate = [NSPredicate predicateWithFormat:@"mediaType = %d", PHAssetMediaTypeImage]; + return options; + } else if ([lowercase isEqualToString:@"videos"]) { + PHFetchOptions *const options = [PHFetchOptions new]; + options.predicate = [NSPredicate predicateWithFormat:@"mediaType = %d", PHAssetMediaTypeVideo]; + return options; + } else { + if (![lowercase isEqualToString:@"all"]) { + RCTLogError(@"Invalid filter option: '%@'. Expected one of 'photos'," + "'videos' or 'all'.", mediaType); + } + // This case includes the "all" mediatype + return nil; } - return filter ?: [_ALAssetsFilter allPhotos]; } @end @@ -111,45 +73,87 @@ @implementation RCTCameraRollManager @synthesize bridge = _bridge; -static NSString *const kErrorUnableToLoad = @"E_UNABLE_TO_LOAD"; static NSString *const kErrorUnableToSave = @"E_UNABLE_TO_SAVE"; +static NSString *const kErrorUnableToLoad = @"E_UNABLE_TO_LOAD"; + +static NSString *const kErrorAuthRestricted = @"E_PHOTO_LIBRARY_AUTH_RESTRICTED"; +static NSString *const kErrorAuthDenied = @"E_PHOTO_LIBRARY_AUTH_DENIED"; + +typedef void (^PhotosAuthorizedBlock)(void); + +static void requestPhotoLibraryAccess(RCTPromiseRejectBlock reject, PhotosAuthorizedBlock authorizedBlock) { + PHAuthorizationStatus authStatus = [PHPhotoLibrary authorizationStatus]; + if (authStatus == PHAuthorizationStatusRestricted) { + reject(kErrorAuthRestricted, @"Access to photo library is restricted", nil); + } else if (authStatus == PHAuthorizationStatusAuthorized) { + authorizedBlock(); + } else if (authStatus == PHAuthorizationStatusNotDetermined) { + [PHPhotoLibrary requestAuthorization:^(PHAuthorizationStatus status) { + requestPhotoLibraryAccess(reject, authorizedBlock); + }]; + } else { + reject(kErrorAuthDenied, @"Access to photo library was denied", nil); + } +} RCT_EXPORT_METHOD(saveToCameraRoll:(NSURLRequest *)request type:(NSString *)type resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) { - if ([type isEqualToString:@"video"]) { - // It's unclear if writeVideoAtPathToSavedPhotosAlbum is thread-safe - dispatch_async(dispatch_get_main_queue(), ^{ - [self->_bridge.assetsLibrary writeVideoAtPathToSavedPhotosAlbum:request.URL completionBlock:^(NSURL *assetURL, NSError *saveError) { - if (saveError) { - reject(kErrorUnableToSave, nil, saveError); - } else { - resolve(assetURL.absoluteString); - } - }]; - }); - } else { - [_bridge.imageLoader loadImageWithURLRequest:request - callback:^(NSError *loadError, UIImage *loadedImage) { - if (loadError) { - reject(kErrorUnableToLoad, nil, loadError); - return; + __block PHObjectPlaceholder *placeholder; + + // We load images and videos differently. + // Images have many custom loaders which can load images from ALAssetsLibrary URLs, PHPhotoLibrary + // URLs, `data:` URIs, etc. Video URLs are passed directly through for now; it may be nice to support + // more ways of loading videos in the future. + __block NSURL *inputURI = nil; + __block UIImage *inputImage = nil; + + void (^saveBlock)(void) = ^void() { + // performChanges and the completionHandler are called on + // arbitrary threads, not the main thread - this is safe + // for now since all JS is queued and executed on a single thread. + // We should reevaluate this if that assumption changes. + [[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{ + PHAssetChangeRequest *changeRequest; + + // Defaults to "photo". `type` is an optional param. + if ([type isEqualToString:@"video"]) { + changeRequest = [PHAssetChangeRequest creationRequestForAssetFromVideoAtFileURL:inputURI]; + } else { + changeRequest = [PHAssetChangeRequest creationRequestForAssetFromImage:inputImage]; + } + + placeholder = [changeRequest placeholderForCreatedAsset]; + } completionHandler:^(BOOL success, NSError * _Nullable error) { + if (success) { + NSString *uri = [NSString stringWithFormat:@"ph://%@", [placeholder localIdentifier]]; + resolve(uri); + } else { + reject(kErrorUnableToSave, nil, error); } - // It's unclear if writeImageToSavedPhotosAlbum is thread-safe - dispatch_async(dispatch_get_main_queue(), ^{ - [self->_bridge.assetsLibrary writeImageToSavedPhotosAlbum:loadedImage.CGImage metadata:nil completionBlock:^(NSURL *assetURL, NSError *saveError) { - if (saveError) { - RCTLogWarn(@"Error saving cropped image: %@", saveError); - reject(kErrorUnableToSave, nil, saveError); - } else { - resolve(assetURL.absoluteString); - } - }]; - }); }]; - } + }; + + void (^loadBlock)(void) = ^void() { + if ([type isEqualToString:@"video"]) { + inputURI = request.URL; + saveBlock(); + } else { + [self.bridge.imageLoader loadImageWithURLRequest:request callback:^(NSError *error, UIImage *image) { + if (error) { + reject(kErrorUnableToLoad, nil, error); + return; + } + + inputImage = image; + saveBlock(); + }]; + } + }; + + requestPhotoLibraryAccess(reject, loadBlock); } static void RCTResolvePromise(RCTPromiseResolveBlock resolve, @@ -181,89 +185,133 @@ static void RCTResolvePromise(RCTPromiseResolveBlock resolve, { checkPhotoLibraryConfig(); - ensureAssetsLibLoaded(); - NSUInteger first = [RCTConvert NSInteger:params[@"first"]]; - NSString *afterCursor = [RCTConvert NSString:params[@"after"]]; - NSString *groupName = [RCTConvert NSString:params[@"groupName"]]; - ALAssetsFilter *assetType = [RCTConvert ALAssetsFilter:params[@"assetType"]]; - ALAssetsGroupType groupTypes = [RCTConvert ALAssetsGroupType:params[@"groupTypes"]]; + NSUInteger const first = [RCTConvert NSInteger:params[@"first"]]; + NSString *const afterCursor = [RCTConvert NSString:params[@"after"]]; + NSString *const groupName = [RCTConvert NSString:params[@"groupName"]]; + NSString *const groupTypes = [[RCTConvert NSString:params[@"groupTypes"]] lowercaseString]; + NSString *const mediaType = [RCTConvert NSString:params[@"assetType"]]; + NSArray *const mimeTypes = [RCTConvert NSStringArray:params[@"mimeTypes"]]; + + // If groupTypes is "all", we want to fetch the SmartAlbum "all photos". Otherwise, all + // other groupTypes values require the "album" collection type. + PHAssetCollectionType const collectionType = ([groupTypes isEqualToString:@"all"] + ? PHAssetCollectionTypeSmartAlbum + : PHAssetCollectionTypeAlbum); + PHAssetCollectionSubtype const collectionSubtype = [RCTConvert PHAssetCollectionSubtype:groupTypes]; + + // Predicate for fetching assets within a collection + PHFetchOptions *const assetFetchOptions = [RCTConvert PHFetchOptionsFromMediaType:mediaType]; + assetFetchOptions.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"creationDate" ascending:NO]]; BOOL __block foundAfter = NO; BOOL __block hasNextPage = NO; BOOL __block resolvedPromise = NO; NSMutableArray *> *assets = [NSMutableArray new]; - [_bridge.assetsLibrary enumerateGroupsWithTypes:groupTypes usingBlock:^(ALAssetsGroup *group, BOOL *stopGroups) { - if (group && (groupName == nil || [groupName isEqualToString:[group valueForProperty:_ALAssetsGroupPropertyName]])) { - - [group setAssetsFilter:assetType]; - [group enumerateAssetsWithOptions:NSEnumerationReverse usingBlock:^(ALAsset *result, NSUInteger index, BOOL *stopAssets) { - if (result) { - NSString *uri = ((NSURL *)[result valueForProperty:_ALAssetPropertyAssetURL]).absoluteString; - if (afterCursor && !foundAfter) { - if ([afterCursor isEqualToString:uri]) { - foundAfter = YES; - } - return; // Skip until we get to the first one + // Filter collection name ("group") + PHFetchOptions *const collectionFetchOptions = [PHFetchOptions new]; + collectionFetchOptions.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"endDate" ascending:NO]]; + if (groupName != nil) { + collectionFetchOptions.predicate = [NSPredicate predicateWithFormat:[NSString stringWithFormat:@"localizedTitle == '%@'", groupName]]; + } + + requestPhotoLibraryAccess(reject, ^{ + PHFetchResult *const assetCollectionFetchResult = [PHAssetCollection fetchAssetCollectionsWithType:collectionType subtype:collectionSubtype options:collectionFetchOptions]; + [assetCollectionFetchResult enumerateObjectsUsingBlock:^(PHAssetCollection * _Nonnull assetCollection, NSUInteger collectionIdx, BOOL * _Nonnull stopCollections) { + // Enumerate assets within the collection + PHFetchResult *const assetsFetchResult = [PHAsset fetchAssetsInAssetCollection:assetCollection options:assetFetchOptions]; + + [assetsFetchResult enumerateObjectsUsingBlock:^(PHAsset * _Nonnull asset, NSUInteger assetIdx, BOOL * _Nonnull stopAssets) { + NSString *const uri = [NSString stringWithFormat:@"ph://%@", [asset localIdentifier]]; + if (afterCursor && !foundAfter) { + if ([afterCursor isEqualToString:uri]) { + foundAfter = YES; } - if (first == assets.count) { - *stopAssets = YES; - *stopGroups = YES; - hasNextPage = YES; - RCTAssert(resolvedPromise == NO, @"Resolved the promise before we finished processing the results."); - RCTResolvePromise(resolve, assets, hasNextPage); - resolvedPromise = YES; + return; // skip until we get to the first one + } + + // Get underlying resources of an asset - this includes files as well as details about edited PHAssets + if ([mimeTypes count] > 0) { + NSArray *const assetResources = [PHAssetResource assetResourcesForAsset:asset]; + if (![assetResources firstObject]) { return; } - CGSize dimensions = [result defaultRepresentation].dimensions; - CLLocation *loc = [result valueForProperty:_ALAssetPropertyLocation]; - NSDate *date = [result valueForProperty:_ALAssetPropertyDate]; - NSString *filename = [result defaultRepresentation].filename; - int64_t duration = 0; - if ([[result valueForProperty:_ALAssetPropertyType] isEqualToString:_ALAssetTypeVideo]) { - duration = [[result valueForProperty:_ALAssetPropertyDuration] intValue]; - } - - [assets addObject:@{ - @"node": @{ - @"type": [result valueForProperty:_ALAssetPropertyType], - @"group_name": [group valueForProperty:_ALAssetsGroupPropertyName], - @"image": @{ - @"uri": uri, - @"filename" : filename ?: [NSNull null], - @"height": @(dimensions.height), - @"width": @(dimensions.width), - @"isStored": @YES, - @"playableDuration": @(duration), - }, - @"timestamp": @(date.timeIntervalSince1970), - @"location": loc ? @{ - @"latitude": @(loc.coordinate.latitude), - @"longitude": @(loc.coordinate.longitude), - @"altitude": @(loc.altitude), - @"heading": @(loc.course), - @"speed": @(loc.speed), - } : @{}, + + PHAssetResource *const _Nonnull resource = [assetResources firstObject]; + CFStringRef const uti = (__bridge CFStringRef _Nonnull)(resource.uniformTypeIdentifier); + NSString *const mimeType = (NSString *)CFBridgingRelease(UTTypeCopyPreferredTagWithClass(uti, kUTTagClassMIMEType)); + + BOOL __block mimeTypeFound = NO; + [mimeTypes enumerateObjectsUsingBlock:^(NSString * _Nonnull mimeTypeFilter, NSUInteger idx, BOOL * _Nonnull stop) { + if ([mimeType isEqualToString:mimeTypeFilter]) { + mimeTypeFound = YES; + *stop = YES; } }]; + + if (!mimeTypeFound) { + return; + } + } + + // If we've accumulated enough results to resolve a single promise + if (first == assets.count) { + *stopAssets = YES; + *stopCollections = YES; + hasNextPage = YES; + RCTAssert(resolvedPromise == NO, @"Resolved the promise before we finished processing the results."); + RCTResolvePromise(resolve, assets, hasNextPage); + resolvedPromise = YES; + return; } + + NSString *const assetMediaTypeLabel = (asset.mediaType == PHAssetMediaTypeVideo + ? @"video" + : (asset.mediaType == PHAssetMediaTypeImage + ? @"image" + : (asset.mediaType == PHAssetMediaTypeAudio + ? @"audio" + : @"unknown"))); + CLLocation *const loc = asset.location; + + // A note on isStored: in the previous code that used ALAssets, isStored + // was always set to YES, probably because iCloud-synced images were never returned (?). + // To get the "isStored" information and filename, we would need to actually request the + // image data from the image manager. Those operations could get really expensive and + // would definitely utilize the disk too much. + // Thus, this field is actually not reliable. + // Note that Android also does not return the `isStored` field at all. + [assets addObject:@{ + @"node": @{ + @"type": assetMediaTypeLabel, // TODO: switch to mimeType? + @"group_name": [assetCollection localizedTitle], + @"image": @{ + @"uri": uri, + @"height": @([asset pixelHeight]), + @"width": @([asset pixelWidth]), + @"isStored": @YES, // this field doesn't seem to exist on android + @"playableDuration": @([asset duration]) // fractional seconds + }, + @"timestamp": @(asset.creationDate.timeIntervalSince1970), + @"location": (loc ? @{ + @"latitude": @(loc.coordinate.latitude), + @"longitude": @(loc.coordinate.longitude), + @"altitude": @(loc.altitude), + @"heading": @(loc.course), + @"speed": @(loc.speed), // speed in m/s + } : @{}) + } + }]; }]; + }]; + + // If we get this far and haven't resolved the promise yet, we reached the end of the list of photos + if (!resolvedPromise) { + hasNextPage = NO; + RCTResolvePromise(resolve, assets, hasNextPage); + resolvedPromise = YES; } - - if (!group) { - // Sometimes the enumeration continues even if we set stop above, so we guard against resolving the promise - // multiple times here. - if (!resolvedPromise) { - RCTResolvePromise(resolve, assets, hasNextPage); - resolvedPromise = YES; - } - } - } failureBlock:^(NSError *error) { - if (error.code != ALAssetsLibraryAccessUserDeniedError) { - RCTLogError(@"Failure while iterating through asset groups %@", error); - } - reject(kErrorUnableToLoad, nil, error); - }]; + }); } RCT_EXPORT_METHOD(deletePhotos:(NSArray*)assets diff --git a/Libraries/CameraRoll/RCTImagePickerManager.h b/Libraries/CameraRoll/RCTImagePickerManager.h index 825e7721a46c01..5f489191c3ffc2 100644 --- a/Libraries/CameraRoll/RCTImagePickerManager.h +++ b/Libraries/CameraRoll/RCTImagePickerManager.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2013-present, Facebook, Inc. + * 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. diff --git a/Libraries/CameraRoll/RCTImagePickerManager.m b/Libraries/CameraRoll/RCTImagePickerManager.m index 6273d7bed9c321..ac6db9b8070b4e 100644 --- a/Libraries/CameraRoll/RCTImagePickerManager.m +++ b/Libraries/CameraRoll/RCTImagePickerManager.m @@ -1,5 +1,5 @@ /* - * Copyright (c) 2013-present, Facebook, Inc. + * 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. @@ -16,6 +16,16 @@ #import #import +@interface RCTImagePickerController : UIImagePickerController + +@property (nonatomic, assign) BOOL unmirrorFrontFacingCamera; + +@end + +@implementation RCTImagePickerController + +@end + @interface RCTImagePickerManager () @end @@ -31,6 +41,22 @@ @implementation RCTImagePickerManager @synthesize bridge = _bridge; +- (id)init +{ + if (self = [super init]) { + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(cameraChanged:) + name:@"AVCaptureDeviceDidStartRunningNotification" + object:nil]; + } + return self; +} + +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self name:@"AVCaptureDeviceDidStartRunningNotification" object:nil]; +} + - (dispatch_queue_t)methodQueue { return dispatch_get_main_queue(); @@ -56,9 +82,12 @@ - (dispatch_queue_t)methodQueue return; } - UIImagePickerController *imagePicker = [UIImagePickerController new]; + RCTImagePickerController *imagePicker = [RCTImagePickerController new]; imagePicker.delegate = self; imagePicker.sourceType = UIImagePickerControllerSourceTypeCamera; + NSArray *availableMediaTypes = [UIImagePickerController availableMediaTypesForSourceType:UIImagePickerControllerSourceTypeCamera]; + imagePicker.mediaTypes = availableMediaTypes; + imagePicker.unmirrorFrontFacingCamera = [RCTConvert BOOL:config[@"unmirrorFrontFacingCamera"]]; if ([RCTConvert BOOL:config[@"videoMode"]]) { imagePicker.cameraCaptureMode = UIImagePickerControllerCameraCaptureModeVideo; @@ -175,4 +204,17 @@ - (void)_dismissPicker:(UIImagePickerController *)picker args:(NSArray *)args } } +- (void)cameraChanged:(NSNotification *)notification +{ + for (UIImagePickerController *picker in _pickers) { + if ([picker isKindOfClass:[RCTImagePickerController class]] + && ((RCTImagePickerController *)picker).unmirrorFrontFacingCamera + && picker.cameraDevice == UIImagePickerControllerCameraDeviceFront) { + picker.cameraViewTransform = CGAffineTransformScale(CGAffineTransformIdentity, -1, 1); + } else { + picker.cameraViewTransform = CGAffineTransformIdentity; + } + } +} + @end diff --git a/Libraries/CameraRoll/RCTPhotoLibraryImageLoader.h b/Libraries/CameraRoll/RCTPhotoLibraryImageLoader.h index 0a45f184dab73c..d3b8dc646b3c6b 100644 --- a/Libraries/CameraRoll/RCTPhotoLibraryImageLoader.h +++ b/Libraries/CameraRoll/RCTPhotoLibraryImageLoader.h @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/CameraRoll/RCTPhotoLibraryImageLoader.m b/Libraries/CameraRoll/RCTPhotoLibraryImageLoader.m index ac27231adc9e9a..ccdf3c27f73087 100644 --- a/Libraries/CameraRoll/RCTPhotoLibraryImageLoader.m +++ b/Libraries/CameraRoll/RCTPhotoLibraryImageLoader.m @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -39,7 +39,7 @@ - (RCTImageLoaderCancellationBlock)loadImageForURL:(NSURL *)imageURL // Using PhotoKit for iOS 8+ // The 'ph://' prefix is used by FBMediaKit to differentiate between // assets-library. It is prepended to the local ID so that it is in the - // form of an, NSURL which is what assets-library uses. + // form of an NSURL which is what assets-library uses. NSString *assetID = @""; PHFetchResult *results; if (!imageURL) { diff --git a/Libraries/CameraRoll/__mocks__/CameraRoll.js b/Libraries/CameraRoll/__mocks__/CameraRoll.js new file mode 100644 index 00000000000000..5094bdc7290099 --- /dev/null +++ b/Libraries/CameraRoll/__mocks__/CameraRoll.js @@ -0,0 +1,14 @@ +/** + * 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 + * @format + */ +'use strict'; + +const CameraRoll = {}; + +export default CameraRoll; diff --git a/Libraries/Color/normalizeColor.js b/Libraries/Color/normalizeColor.js index edede79ee6e267..811b5535f8de5f 100755 --- a/Libraries/Color/normalizeColor.js +++ b/Libraries/Color/normalizeColor.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/Components/AccessibilityInfo/AccessibilityInfo.android.js b/Libraries/Components/AccessibilityInfo/AccessibilityInfo.android.js index 420d0380a2dd8f..6cd0193eb42eca 100644 --- a/Libraries/Components/AccessibilityInfo/AccessibilityInfo.android.js +++ b/Libraries/Components/AccessibilityInfo/AccessibilityInfo.android.js @@ -1,11 +1,11 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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 + * @flow */ 'use strict'; @@ -35,6 +35,8 @@ const _subscriptions = new Map(); */ const AccessibilityInfo = { + /* $FlowFixMe(>=0.78.0 site=react_native_android_fb) This issue was found + * when making Flow check .android.js files. */ fetch: function(): Promise { return new Promise((resolve, reject) => { RCTAccessibilityInfo.isTouchExplorationEnabled(function(resp) { diff --git a/Libraries/Components/AccessibilityInfo/AccessibilityInfo.ios.js b/Libraries/Components/AccessibilityInfo/AccessibilityInfo.ios.js index da1f6e4f1610d1..5a1d64e9595e83 100644 --- a/Libraries/Components/AccessibilityInfo/AccessibilityInfo.ios.js +++ b/Libraries/Components/AccessibilityInfo/AccessibilityInfo.ios.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/Components/ActivityIndicator/ActivityIndicator.js b/Libraries/Components/ActivityIndicator/ActivityIndicator.js index abe6a75af9478e..7d8b9cb77ce863 100644 --- a/Libraries/Components/ActivityIndicator/ActivityIndicator.js +++ b/Libraries/Components/ActivityIndicator/ActivityIndicator.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -15,7 +15,7 @@ const React = require('React'); const StyleSheet = require('StyleSheet'); const View = require('View'); -const requireNativeComponent = require('requireNativeComponent'); +const RCTActivityIndicatorViewNativeComponent = require('RCTActivityIndicatorViewNativeComponent'); import type {NativeComponent} from 'ReactNative'; import type {ViewProps} from 'ViewPropTypes'; @@ -23,7 +23,7 @@ import type {ViewProps} from 'ViewPropTypes'; const RCTActivityIndicator = Platform.OS === 'android' ? require('ProgressBarAndroid') - : requireNativeComponent('RCTActivityIndicatorView'); + : RCTActivityIndicatorViewNativeComponent; const GRAY = '#999999'; @@ -69,10 +69,7 @@ type Props = $ReadOnly<{| * * See http://facebook.github.io/react-native/docs/activityindicator.html */ -const ActivityIndicator = ( - props: Props, - forwardedRef?: ?React.Ref<'RCTActivityIndicatorView'>, -) => { +const ActivityIndicator = (props: Props, forwardedRef?: any) => { const {onLayout, style, ...restProps} = props; let sizeStyle; @@ -103,14 +100,19 @@ const ActivityIndicator = ( styles.container, style, )}> + {/* $FlowFixMe(>=0.78.0 site=react_native_android_fb) This issue was + * found when making Flow check .android.js files. */} ); }; -// $FlowFixMe - TODO T29156721 `React.forwardRef` is not defined in Flow, yet. const ActivityIndicatorWithRef = 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, @@ -133,4 +135,7 @@ const styles = StyleSheet.create({ }, }); +/* $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. */ module.exports = (ActivityIndicatorWithRef: Class>); diff --git a/Libraries/Components/ActivityIndicator/RCTActivityIndicatorViewNativeComponent.js b/Libraries/Components/ActivityIndicator/RCTActivityIndicatorViewNativeComponent.js new file mode 100644 index 00000000000000..461ee0e51d3392 --- /dev/null +++ b/Libraries/Components/ActivityIndicator/RCTActivityIndicatorViewNativeComponent.js @@ -0,0 +1,60 @@ +/** + * 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 requireNativeComponent = require('requireNativeComponent'); + +import type {ViewProps} from 'ViewPropTypes'; +import type {ViewStyleProp} from 'StyleSheet'; +import type {NativeComponent} from 'ReactNative'; + +type NativeProps = $ReadOnly<{| + ...ViewProps, + + /** + * Whether the indicator should hide when not animating (true by default). + * + * See http://facebook.github.io/react-native/docs/activityindicator.html#hideswhenstopped + */ + hidesWhenStopped?: ?boolean, + + /** + * Whether to show the indicator (true, the default) or hide it (false). + * + * See http://facebook.github.io/react-native/docs/activityindicator.html#animating + */ + animating?: ?boolean, + + /** + * The foreground color of the spinner (default is gray). + * + * See http://facebook.github.io/react-native/docs/activityindicator.html#color + */ + color?: ?string, + + /** + * Size of the indicator (default is 'small'). + * Passing a number to the size prop is only supported on Android. + * + * See http://facebook.github.io/react-native/docs/activityindicator.html#size + */ + size?: ?(number | 'small' | 'large'), + + style?: ?ViewStyleProp, + styleAttr?: ?string, + indeterminate?: ?boolean, +|}>; + +type ActivityIndicatorNativeType = Class>; + +module.exports = ((requireNativeComponent( + 'RCTActivityIndicatorView', +): any): ActivityIndicatorNativeType); diff --git a/Libraries/Components/ActivityIndicator/__tests__/ActivityIndicator-test.js b/Libraries/Components/ActivityIndicator/__tests__/ActivityIndicator-test.js new file mode 100644 index 00000000000000..ec97e8255c4f29 --- /dev/null +++ b/Libraries/Components/ActivityIndicator/__tests__/ActivityIndicator-test.js @@ -0,0 +1,54 @@ +/** + * 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 + */ + +'use strict'; + +const React = require('React'); +const ActivityIndicator = require('ActivityIndicator'); +const render = require('../../../../jest/renderer'); + +describe('ActivityIndicator', () => { + it('should set displayName to prevent regressions', () => { + expect(ActivityIndicator.displayName).toBe('ActivityIndicator'); + }); + + it('should render as when mocked', () => { + const instance = render.create( + , + ); + expect(instance).toMatchSnapshot(); + }); + + it('should shallow render as when mocked', () => { + const output = render.shallow( + , + ); + expect(output).toMatchSnapshot(); + }); + + it('should shallow render as when not mocked', () => { + jest.dontMock('ActivityIndicator'); + + const output = render.shallow( + , + ); + expect(output).toMatchSnapshot(); + }); + + it('should render as when not mocked', () => { + jest.dontMock('ActivityIndicator'); + + const instance = render.create( + , + ); + expect(instance).toMatchSnapshot(); + }); +}); diff --git a/Libraries/Components/ActivityIndicator/__tests__/__snapshots__/ActivityIndicator-test.js.snap b/Libraries/Components/ActivityIndicator/__tests__/__snapshots__/ActivityIndicator-test.js.snap new file mode 100644 index 00000000000000..d8cbec0f26428f --- /dev/null +++ b/Libraries/Components/ActivityIndicator/__tests__/__snapshots__/ActivityIndicator-test.js.snap @@ -0,0 +1,54 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ActivityIndicator should render as when mocked 1`] = ` + +`; + +exports[`ActivityIndicator should render as when not mocked 1`] = ` + + + +`; + +exports[`ActivityIndicator should shallow render as when mocked 1`] = ` + +`; + +exports[`ActivityIndicator should shallow render as when not mocked 1`] = ` + +`; diff --git a/Libraries/Components/AppleTV/TVEventHandler.js b/Libraries/Components/AppleTV/TVEventHandler.js index 567776b5d9bfa9..9b584de6bec581 100644 --- a/Libraries/Components/AppleTV/TVEventHandler.js +++ b/Libraries/Components/AppleTV/TVEventHandler.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/Components/AppleTV/TVViewPropTypes.js b/Libraries/Components/AppleTV/TVViewPropTypes.js index 5bda1f618c5440..d7492967bc128b 100644 --- a/Libraries/Components/AppleTV/TVViewPropTypes.js +++ b/Libraries/Components/AppleTV/TVViewPropTypes.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -9,76 +9,100 @@ */ 'use strict'; -const PropTypes = require('prop-types'); + +export type TVParallaxPropertiesType = $ReadOnly<{| + /** + * If true, parallax effects are enabled. Defaults to true. + */ + enabled?: boolean, + + /** + * Defaults to 2.0. + */ + shiftDistanceX?: number, + + /** + * Defaults to 2.0. + */ + shiftDistanceY?: number, + + /** + * Defaults to 0.05. + */ + tiltAngle?: number, + + /** + * Defaults to 1.0 + */ + magnification?: number, + + /** + * Defaults to 1.0 + */ + pressMagnification?: number, + + /** + * Defaults to 0.3 + */ + pressDuration?: number, + + /** + * Defaults to 0.3 + */ + pressDelay?: number, +|}>; /** * Additional View properties for Apple TV */ -const TVViewPropTypes = { +export type TVViewProps = $ReadOnly<{| /** - * When set to true, this view will be focusable - * and navigable using the TV remote. + * *(Apple TV only)* When set to true, this view will be focusable + * and navigable using the Apple TV remote. + * + * @platform ios */ - isTVSelectable: PropTypes.bool, + isTVSelectable?: boolean, /** - * May be set to true to force the TV focus engine to move focus to this view. + * *(Apple TV only)* May be set to true to force the Apple TV focus engine to move focus to this view. + * + * @platform ios */ - hasTVPreferredFocus: PropTypes.bool, + hasTVPreferredFocus?: boolean, /** * *(Apple TV only)* Object with properties to control Apple TV parallax effects. * - * enabled: If true, parallax effects are enabled. Defaults to true. - * shiftDistanceX: Defaults to 2.0. - * shiftDistanceY: Defaults to 2.0. - * tiltAngle: Defaults to 0.05. - * magnification: Defaults to 1.0. - * pressMagnification: Defaults to 1.0. - * pressDuration: Defaults to 0.3. - * pressDelay: Defaults to 0.0. - * * @platform ios */ - tvParallaxProperties: PropTypes.object, + tvParallaxProperties?: TVParallaxPropertiesType, /** * *(Apple TV only)* May be used to change the appearance of the Apple TV parallax effect when this view goes in or out of focus. Defaults to 2.0. * * @platform ios */ - tvParallaxShiftDistanceX: PropTypes.number, + tvParallaxShiftDistanceX?: number, /** * *(Apple TV only)* May be used to change the appearance of the Apple TV parallax effect when this view goes in or out of focus. Defaults to 2.0. * * @platform ios */ - tvParallaxShiftDistanceY: PropTypes.number, + tvParallaxShiftDistanceY?: number, /** * *(Apple TV only)* May be used to change the appearance of the Apple TV parallax effect when this view goes in or out of focus. Defaults to 0.05. * * @platform ios */ - tvParallaxTiltAngle: PropTypes.number, + tvParallaxTiltAngle?: number, /** * *(Apple TV only)* May be used to change the appearance of the Apple TV parallax effect when this view goes in or out of focus. Defaults to 1.0. * * @platform ios */ - tvParallaxMagnification: PropTypes.number, -}; - -export type TVViewProps = $ReadOnly<{| - isTVSelectable?: boolean, - hasTVPreferredFocus?: boolean, - tvParallaxProperties?: Object, - tvParallaxShiftDistanceX?: number, - tvParallaxShiftDistanceY?: number, - tvParallaxTiltAngle?: number, tvParallaxMagnification?: number, |}>; - -module.exports = TVViewPropTypes; diff --git a/Libraries/Components/Button.js b/Libraries/Components/Button.js index 7e1140cf02ae93..78e6bdd636dab8 100644 --- a/Libraries/Components/Button.js +++ b/Libraries/Components/Button.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -10,17 +10,54 @@ 'use strict'; -const ColorPropType = require('ColorPropType'); const Platform = require('Platform'); const React = require('React'); -const PropTypes = require('prop-types'); const StyleSheet = require('StyleSheet'); const Text = require('Text'); const TouchableNativeFeedback = require('TouchableNativeFeedback'); const TouchableOpacity = require('TouchableOpacity'); const View = require('View'); -const invariant = require('fbjs/lib/invariant'); +const invariant = require('invariant'); + +import type {PressEvent} from 'CoreEventTypes'; + +type ButtonProps = $ReadOnly<{| + /** + * Text to display inside the button + */ + title: string, + + /** + * Handler to be called when the user taps the button + */ + onPress: (event?: PressEvent) => mixed, + + /** + * Color of the text (iOS), or background color of the button (Android) + */ + color?: ?string, + + /** + * TV preferred focus (see documentation for the View component). + */ + hasTVPreferredFocus?: ?boolean, + + /** + * Text to display for blindness accessibility features + */ + accessibilityLabel?: ?string, + + /** + * If true, disable all interactions for this component. + */ + disabled?: ?boolean, + + /** + * Used to locate this view in end-to-end tests. + */ + testID?: ?string, +|}>; /** * A basic button component that should render nicely on any platform. Supports @@ -50,46 +87,7 @@ const invariant = require('fbjs/lib/invariant'); * */ -class Button extends React.Component<{ - title: string, - onPress: () => any, - color?: ?string, - hasTVPreferredFocus?: ?boolean, - accessibilityLabel?: ?string, - disabled?: ?boolean, - testID?: ?string, -}> { - static propTypes = { - /** - * Text to display inside the button - */ - title: PropTypes.string.isRequired, - /** - * Text to display for blindness accessibility features - */ - accessibilityLabel: PropTypes.string, - /** - * Color of the text (iOS), or background color of the button (Android) - */ - color: ColorPropType, - /** - * If true, disable all interactions for this component. - */ - disabled: PropTypes.bool, - /** - * TV preferred focus (see documentation for the View component). - */ - hasTVPreferredFocus: PropTypes.bool, - /** - * Handler to be called when the user taps the button - */ - onPress: PropTypes.func.isRequired, - /** - * Used to locate this view in end-to-end tests. - */ - testID: PropTypes.string, - }; - +class Button extends React.Component { render() { const { accessibilityLabel, @@ -152,21 +150,21 @@ const styles = StyleSheet.create({ borderRadius: 2, }, }), - text: Platform.select({ - ios: { - // iOS blue from https://developer.apple.com/ios/human-interface-guidelines/visual-design/color/ - color: '#007AFF', - textAlign: 'center', - padding: 8, - fontSize: 18, - }, - android: { - color: 'white', - textAlign: 'center', - padding: 8, - fontWeight: '500', - }, - }), + text: { + textAlign: 'center', + padding: 8, + ...Platform.select({ + ios: { + // iOS blue from https://developer.apple.com/ios/human-interface-guidelines/visual-design/color/ + color: '#007AFF', + fontSize: 18, + }, + android: { + color: 'white', + fontWeight: '500', + }, + }), + }, buttonDisabled: Platform.select({ ios: {}, android: { diff --git a/Libraries/Components/CheckBox/AndroidCheckBoxNativeComponent.js b/Libraries/Components/CheckBox/AndroidCheckBoxNativeComponent.js new file mode 100644 index 00000000000000..14c84b069f8531 --- /dev/null +++ b/Libraries/Components/CheckBox/AndroidCheckBoxNativeComponent.js @@ -0,0 +1,51 @@ +/** + * 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 + * @format + */ +'use strict'; + +const requireNativeComponent = require('requireNativeComponent'); + +import type {ViewProps} from 'ViewPropTypes'; +import type {SyntheticEvent} from 'CoreEventTypes'; +import type {NativeComponent} from 'ReactNative'; + +type CheckBoxEvent = SyntheticEvent< + $ReadOnly<{| + target: number, + value: boolean, + |}>, +>; + +type NativeProps = $ReadOnly<{| + ...ViewProps, + + /** + * Used in case the props change removes the component. + */ + onChange?: ?(event: CheckBoxEvent) => mixed, + + /** + * Invoked with the new value when the value changes. + */ + onValueChange?: ?(value: boolean) => mixed, + + /** + * Used to locate this view in end-to-end tests. + */ + testID?: ?string, + + on?: ?boolean, + enabled?: boolean, +|}>; + +type CheckBoxNativeType = Class>; + +module.exports = ((requireNativeComponent( + 'AndroidCheckBox', +): any): CheckBoxNativeType); diff --git a/Libraries/Components/CheckBox/CheckBox.android.js b/Libraries/Components/CheckBox/CheckBox.android.js index e022013484c18b..e288562a0be0e3 100644 --- a/Libraries/Components/CheckBox/CheckBox.android.js +++ b/Libraries/Components/CheckBox/CheckBox.android.js @@ -1,29 +1,80 @@ /** - * Copyright (c) 2017-present, Facebook, Inc. + * 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 + * @flow strict-local * @format */ 'use strict'; -const NativeMethodsMixin = require('NativeMethodsMixin'); -const PropTypes = require('prop-types'); const React = require('React'); const StyleSheet = require('StyleSheet'); -const ViewPropTypes = require('ViewPropTypes'); -const createReactClass = require('create-react-class'); -const requireNativeComponent = require('requireNativeComponent'); +const AndroidCheckBoxNativeComponent = require('AndroidCheckBoxNativeComponent'); +const nullthrows = require('nullthrows'); +const setAndForwardRef = require('setAndForwardRef'); -const RCTCheckBox = requireNativeComponent('AndroidCheckBox'); +import type {ViewProps} from 'ViewPropTypes'; +import type {SyntheticEvent} from 'CoreEventTypes'; +import type {NativeComponent} from 'ReactNative'; -type DefaultProps = { - value: boolean, - disabled: boolean, -}; +type CheckBoxEvent = SyntheticEvent< + $ReadOnly<{| + target: number, + value: boolean, + |}>, +>; + +type CommonProps = $ReadOnly<{| + ...ViewProps, + + /** + * Used in case the props change removes the component. + */ + onChange?: ?(event: CheckBoxEvent) => mixed, + + /** + * Invoked with the new value when the value changes. + */ + onValueChange?: ?(value: boolean) => mixed, + + /** + * Used to locate this view in end-to-end tests. + */ + testID?: ?string, +|}>; + +type NativeProps = $ReadOnly<{| + ...CommonProps, + + on?: ?boolean, + enabled?: boolean, +|}>; + +type CheckBoxNativeType = Class>; + +type Props = $ReadOnly<{| + ...CommonProps, + + /** + * The value of the checkbox. If true the checkbox will be turned on. + * Default value is false. + */ + value?: ?boolean, + + /** + * If true the user won't be able to toggle the checkbox. + * Default value is false. + */ + disabled?: ?boolean, + + /** + * Used to get the ref for the native checkbox + */ + forwardedRef?: ?React.Ref, +|}>; /** * Renders a boolean input (Android only). @@ -80,81 +131,66 @@ type DefaultProps = { * @keyword checkbox * @keyword toggle */ -let CheckBox = createReactClass({ - displayName: 'CheckBox', - propTypes: { - ...ViewPropTypes, - /** - * The value of the checkbox. If true the checkbox will be turned on. - * Default value is false. - */ - value: PropTypes.bool, - /** - * If true the user won't be able to toggle the checkbox. - * Default value is false. - */ - disabled: PropTypes.bool, - /** - * Used in case the props change removes the component. - */ - onChange: PropTypes.func, - /** - * Invoked with the new value when the value changes. - */ - onValueChange: PropTypes.func, - /** - * Used to locate this view in end-to-end tests. - */ - testID: PropTypes.string, - }, - - getDefaultProps: function(): DefaultProps { - return { - value: false, - disabled: false, - }; - }, +class CheckBox extends React.Component { + _nativeRef: ?React.ElementRef = null; + _setNativeRef = setAndForwardRef({ + getForwardedRef: () => this.props.forwardedRef, + setLocalRef: ref => { + this._nativeRef = ref; + }, + }); - mixins: [NativeMethodsMixin], - - _rctCheckBox: {}, - _onChange: function(event: Object) { - this._rctCheckBox.setNativeProps({value: this.props.value}); + _onChange = (event: CheckBoxEvent) => { + const value = this.props.value ?? false; + nullthrows(this._nativeRef).setNativeProps({value: value}); // Change the props after the native props are set in case the props // change removes the component this.props.onChange && this.props.onChange(event); this.props.onValueChange && this.props.onValueChange(event.nativeEvent.value); - }, + }; - render: function() { - let props = {...this.props}; - props.onStartShouldSetResponder = () => true; - props.onResponderTerminationRequest = () => false; - props.enabled = !this.props.disabled; - props.on = this.props.value; - props.style = [styles.rctCheckBox, this.props.style]; + render() { + const {disabled: _, value: __, style, forwardedRef, ...props} = this.props; + const disabled = this.props.disabled ?? false; + const value = this.props.value ?? false; + + const nativeProps = { + ...props, + onStartShouldSetResponder: () => true, + onResponderTerminationRequest: () => false, + enabled: !disabled, + on: value, + style: [styles.rctCheckBox, style], + }; 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. */ - this._rctCheckBox = ref; - }} + ); - }, -}); + } +} -let styles = StyleSheet.create({ +const styles = StyleSheet.create({ rctCheckBox: { height: 32, width: 32, }, }); -module.exports = CheckBox; +/** + * Can't use CheckBoxNativeType because it has different props + */ +type CheckBoxType = Class>; + +const CheckBoxWithRef = React.forwardRef(function CheckBoxWithRef(props, ref) { + return ; +}); + +/* $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. */ +module.exports = (CheckBoxWithRef: CheckBoxType); diff --git a/Libraries/Components/CheckBox/CheckBox.ios.js b/Libraries/Components/CheckBox/CheckBox.ios.js index 5fda2cd5ec2bce..8bc431d610e066 100644 --- a/Libraries/Components/CheckBox/CheckBox.ios.js +++ b/Libraries/Components/CheckBox/CheckBox.ios.js @@ -1,10 +1,10 @@ /** - * Copyright (c) 2017-present, Facebook, Inc. + * 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 + * @flow strict-local * @format */ 'use strict'; diff --git a/Libraries/Components/Clipboard/Clipboard.js b/Libraries/Components/Clipboard/Clipboard.js index 706bc0fba8c9b7..d9eddb6cd0548e 100644 --- a/Libraries/Components/Clipboard/Clipboard.js +++ b/Libraries/Components/Clipboard/Clipboard.js @@ -1,11 +1,11 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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 + * @flow strict-local */ 'use strict'; diff --git a/Libraries/Components/DatePicker/DatePickerIOS.android.js b/Libraries/Components/DatePicker/DatePickerIOS.android.js index 585be6e297673d..c532f212d6080b 100644 --- a/Libraries/Components/DatePicker/DatePickerIOS.android.js +++ b/Libraries/Components/DatePicker/DatePickerIOS.android.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/Components/DatePicker/DatePickerIOS.ios.js b/Libraries/Components/DatePicker/DatePickerIOS.ios.js index 1202364665fc94..a93516c9a1cf17 100644 --- a/Libraries/Components/DatePicker/DatePickerIOS.ios.js +++ b/Libraries/Components/DatePicker/DatePickerIOS.ios.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -8,23 +8,27 @@ * This is a controlled component version of RCTDatePickerIOS * * @format - * @flow + * @flow strict-local */ 'use strict'; const React = require('React'); -const invariant = require('fbjs/lib/invariant'); const StyleSheet = require('StyleSheet'); const View = require('View'); -const requireNativeComponent = require('requireNativeComponent'); +const invariant = require('invariant'); import type {ViewProps} from 'ViewPropTypes'; +import type {SyntheticEvent} from 'CoreEventTypes'; -const RCTDatePickerIOS = requireNativeComponent('RCTDatePicker'); +const RCTDatePickerNativeComponent = require('RCTDatePickerNativeComponent'); -type Event = Object; +type Event = SyntheticEvent< + $ReadOnly<{| + timestamp: number, + |}>, +>; type Props = $ReadOnly<{| ...ViewProps, @@ -113,8 +117,7 @@ class DatePickerIOS extends React.Component { mode: 'datetime', }; - // $FlowFixMe How to type a native component to be able to call setNativeProps - _picker: ?React.ElementRef = null; + _picker: ?React.ElementRef = null; componentDidUpdate() { if (this.props.date) { @@ -142,7 +145,8 @@ class DatePickerIOS extends React.Component { ); return ( - { this._picker = picker; }} @@ -154,7 +158,11 @@ class DatePickerIOS extends React.Component { ? props.initialDate.getTime() : undefined } - locale={props.locale ? props.locale : undefined} + locale={ + props.locale != null && props.locale !== '' + ? props.locale + : undefined + } maximumDate={ props.maximumDate ? props.maximumDate.getTime() : undefined } diff --git a/Libraries/Components/DatePicker/RCTDatePickerNativeComponent.js b/Libraries/Components/DatePicker/RCTDatePickerNativeComponent.js new file mode 100644 index 00000000000000..137d88351314c7 --- /dev/null +++ b/Libraries/Components/DatePicker/RCTDatePickerNativeComponent.js @@ -0,0 +1,41 @@ +/** + * 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 requireNativeComponent = require('requireNativeComponent'); + +import type {SyntheticEvent} from 'CoreEventTypes'; +import type {ViewProps} from 'ViewPropTypes'; +import type {NativeComponent} from 'ReactNative'; + +type Event = SyntheticEvent< + $ReadOnly<{| + timestamp: number, + |}>, +>; + +type NativeProps = $ReadOnly<{| + ...ViewProps, + date?: ?number, + initialDate?: ?Date, + locale?: ?string, + maximumDate?: ?number, + minimumDate?: ?number, + minuteInterval?: ?(1 | 2 | 3 | 4 | 5 | 6 | 10 | 12 | 15 | 20 | 30), + mode?: ?('date' | 'time' | 'datetime'), + onChange?: ?(event: Event) => void, + timeZoneOffsetInMinutes?: ?number, +|}>; +type RCTDatePickerNativeType = Class>; + +module.exports = ((requireNativeComponent( + 'RCTDatePicker', +): any): RCTDatePickerNativeType); diff --git a/Libraries/Components/DatePickerAndroid/DatePickerAndroid.android.js b/Libraries/Components/DatePickerAndroid/DatePickerAndroid.android.js index c8f7fb84304ef9..2009731b3b6862 100644 --- a/Libraries/Components/DatePickerAndroid/DatePickerAndroid.android.js +++ b/Libraries/Components/DatePickerAndroid/DatePickerAndroid.android.js @@ -1,21 +1,22 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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 + * @flow strict-local */ 'use strict'; const DatePickerModule = require('NativeModules').DatePickerAndroid; +import type {Options, DatePickerOpenAction} from 'DatePickerAndroidTypes'; /** * Convert a Date to a timestamp. */ -function _toMillis(options: Object, key: string) { +function _toMillis(options: Options, key: string) { const dateVal = options[key]; // Is it a Date object? if (typeof dateVal === 'object' && typeof dateVal.getMonth === 'function') { @@ -65,12 +66,12 @@ class DatePickerAndroid { * Note the native date picker dialog has some UI glitches on Android 4 and lower * when using the `minDate` and `maxDate` options. */ - static async open(options: Object): Promise { + static async open(options: ?Options): Promise { const optionsMs = options; - if (optionsMs) { - _toMillis(options, 'date'); - _toMillis(options, 'minDate'); - _toMillis(options, 'maxDate'); + if (optionsMs != null) { + _toMillis(optionsMs, 'date'); + _toMillis(optionsMs, 'minDate'); + _toMillis(optionsMs, 'maxDate'); } return DatePickerModule.open(options); } @@ -78,15 +79,11 @@ class DatePickerAndroid { /** * A date has been selected. */ - static get dateSetAction() { - return 'dateSetAction'; - } + static +dateSetAction: 'dateSetAction' = 'dateSetAction'; /** * The dialog has been dismissed. */ - static get dismissedAction() { - return 'dismissedAction'; - } + static +dismissedAction: 'dismissedAction' = 'dismissedAction'; } module.exports = DatePickerAndroid; diff --git a/Libraries/Components/DatePickerAndroid/DatePickerAndroid.ios.js b/Libraries/Components/DatePickerAndroid/DatePickerAndroid.ios.js index a158f035c303d3..b693b721356c52 100644 --- a/Libraries/Components/DatePickerAndroid/DatePickerAndroid.ios.js +++ b/Libraries/Components/DatePickerAndroid/DatePickerAndroid.ios.js @@ -1,21 +1,30 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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 + * @flow strict-local */ 'use strict'; -const DatePickerAndroid = { - async open(options: Object): Promise { - return Promise.reject({ - message: 'DatePickerAndroid is not supported on this platform.', - }); - }, -}; +import type {Options, DatePickerOpenAction} from 'DatePickerAndroidTypes'; + +class DatePickerAndroid { + static async open(options: ?Options): Promise { + throw new Error('DatePickerAndroid is not supported on this platform.'); + } + + /** + * A date has been selected. + */ + static +dateSetAction: 'dateSetAction' = 'dateSetAction'; + /** + * The dialog has been dismissed. + */ + static +dismissedAction: 'dismissedAction' = 'dismissedAction'; +} module.exports = DatePickerAndroid; diff --git a/Libraries/Components/DatePickerAndroid/DatePickerAndroidTypes.js b/Libraries/Components/DatePickerAndroid/DatePickerAndroidTypes.js new file mode 100644 index 00000000000000..eedc99d229aa68 --- /dev/null +++ b/Libraries/Components/DatePickerAndroid/DatePickerAndroidTypes.js @@ -0,0 +1,30 @@ +/** + * 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 + */ + +export type Options = $ReadOnly<{| + date?: ?(Date | number), + minDate?: ?(Date | number), + maxDate?: ?(Date | number), + mode?: ?('calender' | 'spinner' | 'default'), +|}>; + +export type DatePickerOpenAction = + | {| + action: 'dateSetAction', + year: number, + month: number, + day: number, + |} + | {| + action: 'dismissedAction', + year: void, + month: void, + day: void, + |}; diff --git a/Libraries/Components/DrawerAndroid/AndroidDrawerLayoutNativeComponent.js b/Libraries/Components/DrawerAndroid/AndroidDrawerLayoutNativeComponent.js new file mode 100644 index 00000000000000..ba5ec7698802e9 --- /dev/null +++ b/Libraries/Components/DrawerAndroid/AndroidDrawerLayoutNativeComponent.js @@ -0,0 +1,121 @@ +/** + * 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 requireNativeComponent = require('requireNativeComponent'); + +import type {NativeComponent} from 'ReactNative'; +import type {SyntheticEvent} from 'CoreEventTypes'; +import type {ViewStyleProp} from 'StyleSheet'; +import type React from 'React'; + +type ColorValue = null | string; + +type DrawerStates = 'Idle' | 'Dragging' | 'Settling'; + +type DrawerStateEvent = SyntheticEvent< + $ReadOnly<{| + drawerState: number, + |}>, +>; + +type DrawerSlideEvent = SyntheticEvent< + $ReadOnly<{| + offset: number, + |}>, +>; + +type NativeProps = $ReadOnly<{| + /** + * Determines whether the keyboard gets dismissed in response to a drag. + * - 'none' (the default), drags do not dismiss the keyboard. + * - 'on-drag', the keyboard is dismissed when a drag begins. + */ + keyboardDismissMode?: ?('none' | 'on-drag'), + + /** + * Specifies the background color of the drawer. The default value is white. + * If you want to set the opacity of the drawer, use rgba. Example: + * + * ``` + * return ( + * + * + * ); + * ``` + */ + drawerBackgroundColor: ColorValue, + + /** + * Specifies the side of the screen from which the drawer will slide in. + */ + drawerPosition: ?number, + + /** + * Specifies the width of the drawer, more precisely the width of the view that be pulled in + * from the edge of the window. + */ + drawerWidth?: ?number, + + /** + * Specifies the lock mode of the drawer. The drawer can be locked in 3 states: + * - unlocked (default), meaning that the drawer will respond (open/close) to touch gestures. + * - locked-closed, meaning that the drawer will stay closed and not respond to gestures. + * - locked-open, meaning that the drawer will stay opened and not respond to gestures. + * The drawer may still be opened and closed programmatically (`openDrawer`/`closeDrawer`). + */ + drawerLockMode?: ?('unlocked' | 'locked-closed' | 'locked-open'), + + /** + * Function called whenever there is an interaction with the navigation view. + */ + onDrawerSlide?: ?(event: DrawerSlideEvent) => mixed, + + /** + * Function called when the drawer state has changed. The drawer can be in 3 states: + * - Idle, meaning there is no interaction with the navigation view happening at the time + * - Dragging, meaning there is currently an interaction with the navigation view + * - Settling, meaning that there was an interaction with the navigation view, and the + * navigation view is now finishing its closing or opening animation + */ + onDrawerStateChanged?: ?(state: DrawerStateEvent) => mixed, + + /** + * Function called whenever the navigation view has been opened. + */ + onDrawerOpen?: ?() => mixed, + + /** + * Function called whenever the navigation view has been closed. + */ + onDrawerClose?: ?() => mixed, + + /** + * The navigation view that will be rendered to the side of the screen and can be pulled in. + */ + renderNavigationView: () => React.Element, + + /** + * Make the drawer take the entire screen and draw the background of the + * status bar to allow it to open over the status bar. It will only have an + * effect on API 21+. + */ + statusBarBackgroundColor?: ?ColorValue, + + children?: React.Node, + style?: ?ViewStyleProp, +|}>; + +type AndroidDrawerLayoutNativeType = Class>; + +module.exports = ((requireNativeComponent( + 'AndroidDrawerLayout', +): any): AndroidDrawerLayoutNativeType); diff --git a/Libraries/Components/DrawerAndroid/DrawerLayoutAndroid.android.js b/Libraries/Components/DrawerAndroid/DrawerLayoutAndroid.android.js index d4b3fa9f7f63d0..2c1e970ca5fe9c 100644 --- a/Libraries/Components/DrawerAndroid/DrawerLayoutAndroid.android.js +++ b/Libraries/Components/DrawerAndroid/DrawerLayoutAndroid.android.js @@ -1,37 +1,140 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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 * @format */ 'use strict'; -const ColorPropType = require('ColorPropType'); -const NativeMethodsMixin = require('NativeMethodsMixin'); const Platform = require('Platform'); const React = require('React'); -const PropTypes = require('prop-types'); const ReactNative = require('ReactNative'); const StatusBar = require('StatusBar'); const StyleSheet = require('StyleSheet'); const UIManager = require('UIManager'); const View = require('View'); -const ViewPropTypes = require('ViewPropTypes'); +const nullthrows = require('nullthrows'); -const DrawerConsts = UIManager.AndroidDrawerLayout.Constants; +const DrawerConsts = UIManager.getViewManagerConfig('AndroidDrawerLayout') + .Constants; -const createReactClass = require('create-react-class'); const dismissKeyboard = require('dismissKeyboard'); -const requireNativeComponent = require('requireNativeComponent'); - -const RK_DRAWER_REF = 'drawerlayout'; -const INNERVIEW_REF = 'innerView'; +const AndroidDrawerLayoutNativeComponent = require('AndroidDrawerLayoutNativeComponent'); const DRAWER_STATES = ['Idle', 'Dragging', 'Settling']; +import type {ViewStyleProp} from 'StyleSheet'; +import type {ColorValue} from 'StyleSheetTypes'; +import type {SyntheticEvent} from 'CoreEventTypes'; +import type { + MeasureOnSuccessCallback, + MeasureInWindowOnSuccessCallback, + MeasureLayoutOnSuccessCallback, +} from 'ReactNativeTypes'; + +type DrawerStates = 'Idle' | 'Dragging' | 'Settling'; + +type DrawerStateEvent = SyntheticEvent< + $ReadOnly<{| + drawerState: number, + |}>, +>; + +type DrawerSlideEvent = SyntheticEvent< + $ReadOnly<{| + offset: number, + |}>, +>; + +type Props = $ReadOnly<{| + /** + * Determines whether the keyboard gets dismissed in response to a drag. + * - 'none' (the default), drags do not dismiss the keyboard. + * - 'on-drag', the keyboard is dismissed when a drag begins. + */ + keyboardDismissMode?: ?('none' | 'on-drag'), + + /** + * Specifies the background color of the drawer. The default value is white. + * If you want to set the opacity of the drawer, use rgba. Example: + * + * ``` + * return ( + * + * + * ); + * ``` + */ + drawerBackgroundColor: ColorValue, + + /** + * Specifies the side of the screen from which the drawer will slide in. + */ + drawerPosition: ?number, + + /** + * Specifies the width of the drawer, more precisely the width of the view that be pulled in + * from the edge of the window. + */ + drawerWidth?: ?number, + + /** + * Specifies the lock mode of the drawer. The drawer can be locked in 3 states: + * - unlocked (default), meaning that the drawer will respond (open/close) to touch gestures. + * - locked-closed, meaning that the drawer will stay closed and not respond to gestures. + * - locked-open, meaning that the drawer will stay opened and not respond to gestures. + * The drawer may still be opened and closed programmatically (`openDrawer`/`closeDrawer`). + */ + drawerLockMode?: ?('unlocked' | 'locked-closed' | 'locked-open'), + + /** + * Function called whenever there is an interaction with the navigation view. + */ + onDrawerSlide?: ?(event: DrawerSlideEvent) => mixed, + + /** + * Function called when the drawer state has changed. The drawer can be in 3 states: + * - Idle, meaning there is no interaction with the navigation view happening at the time + * - Dragging, meaning there is currently an interaction with the navigation view + * - Settling, meaning that there was an interaction with the navigation view, and the + * navigation view is now finishing its closing or opening animation + */ + onDrawerStateChanged?: ?(state: DrawerStates) => mixed, + + /** + * Function called whenever the navigation view has been opened. + */ + onDrawerOpen?: ?() => mixed, + + /** + * Function called whenever the navigation view has been closed. + */ + onDrawerClose?: ?() => mixed, + + /** + * The navigation view that will be rendered to the side of the screen and can be pulled in. + */ + renderNavigationView: () => React.Element, + + /** + * Make the drawer take the entire screen and draw the background of the + * status bar to allow it to open over the status bar. It will only have an + * effect on API 21+. + */ + statusBarBackgroundColor?: ?ColorValue, + + children?: React.Node, + style?: ?ViewStyleProp, +|}>; + +type State = {| + statusBarBackgroundColor: ColorValue, +|}; + /** * React component that wraps the platform `DrawerLayout` (Android only). The * Drawer (typically used for navigation) is rendered with `renderNavigationView` @@ -63,109 +166,18 @@ const DRAWER_STATES = ['Idle', 'Dragging', 'Settling']; * }, * ``` */ -const DrawerLayoutAndroid = createReactClass({ - displayName: 'DrawerLayoutAndroid', - statics: { - positions: DrawerConsts.DrawerPosition, - }, +class DrawerLayoutAndroid extends React.Component { + static positions = DrawerConsts.DrawerPosition; + static defaultProps = { + drawerBackgroundColor: 'white', + }; - propTypes: { - ...ViewPropTypes, - /** - * Determines whether the keyboard gets dismissed in response to a drag. - * - 'none' (the default), drags do not dismiss the keyboard. - * - 'on-drag', the keyboard is dismissed when a drag begins. - */ - keyboardDismissMode: PropTypes.oneOf([ - 'none', // default - 'on-drag', - ]), - /** - * Specifies the background color of the drawer. The default value is white. - * If you want to set the opacity of the drawer, use rgba. Example: - * - * ``` - * return ( - * - * - * ); - * ``` - */ - drawerBackgroundColor: ColorPropType, - /** - * Specifies the side of the screen from which the drawer will slide in. - */ - drawerPosition: PropTypes.oneOf([ - DrawerConsts.DrawerPosition.Left, - DrawerConsts.DrawerPosition.Right, - ]), - /** - * Specifies the width of the drawer, more precisely the width of the view that be pulled in - * from the edge of the window. - */ - drawerWidth: PropTypes.number, - /** - * Specifies the lock mode of the drawer. The drawer can be locked in 3 states: - * - unlocked (default), meaning that the drawer will respond (open/close) to touch gestures. - * - locked-closed, meaning that the drawer will stay closed and not respond to gestures. - * - locked-open, meaning that the drawer will stay opened and not respond to gestures. - * The drawer may still be opened and closed programmatically (`openDrawer`/`closeDrawer`). - */ - drawerLockMode: PropTypes.oneOf([ - 'unlocked', - 'locked-closed', - 'locked-open', - ]), - /** - * Function called whenever there is an interaction with the navigation view. - */ - onDrawerSlide: PropTypes.func, - /** - * Function called when the drawer state has changed. The drawer can be in 3 states: - * - idle, meaning there is no interaction with the navigation view happening at the time - * - dragging, meaning there is currently an interaction with the navigation view - * - settling, meaning that there was an interaction with the navigation view, and the - * navigation view is now finishing its closing or opening animation - */ - onDrawerStateChanged: PropTypes.func, - /** - * Function called whenever the navigation view has been opened. - */ - onDrawerOpen: PropTypes.func, - /** - * Function called whenever the navigation view has been closed. - */ - onDrawerClose: PropTypes.func, - /** - * The navigation view that will be rendered to the side of the screen and can be pulled in. - */ - renderNavigationView: PropTypes.func.isRequired, - - /** - * Make the drawer take the entire screen and draw the background of the - * status bar to allow it to open over the status bar. It will only have an - * effect on API 21+. - */ - statusBarBackgroundColor: ColorPropType, - }, + _nativeRef = React.createRef>>(); - mixins: [NativeMethodsMixin], + state = {statusBarBackgroundColor: null}; - getDefaultProps: function(): Object { - return { - drawerBackgroundColor: 'white', - }; - }, - - getInitialState: function() { - return {statusBarBackgroundColor: undefined}; - }, - - getInnerViewNode: function() { - return this.refs[INNERVIEW_REF].getInnerViewNode(); - }, - - render: function() { + render() { + const {onDrawerStateChanged, ...props} = this.props; const drawStatusBar = Platform.Version >= 21 && this.props.statusBarBackgroundColor; const drawerViewWrapper = ( @@ -183,7 +195,7 @@ const DrawerLayoutAndroid = createReactClass({ ); const childrenWrapper = ( - + {drawStatusBar && ( ); return ( - =0.87.0 site=react_native_android_fb) This comment + * suppresses an error found when Flow v0.87 was deployed. To see the + * error, delete this comment and run Flow. */ + ref={this._nativeRef} drawerWidth={this.props.drawerWidth} drawerPosition={this.props.drawerPosition} drawerLockMode={this.props.drawerLockMode} @@ -215,60 +230,62 @@ const DrawerLayoutAndroid = createReactClass({ onDrawerStateChanged={this._onDrawerStateChanged}> {childrenWrapper} {drawerViewWrapper} - + ); - }, + } - _onDrawerSlide: function(event) { + _onDrawerSlide = (event: DrawerSlideEvent) => { if (this.props.onDrawerSlide) { this.props.onDrawerSlide(event); } if (this.props.keyboardDismissMode === 'on-drag') { dismissKeyboard(); } - }, + }; - _onDrawerOpen: function() { + _onDrawerOpen = () => { if (this.props.onDrawerOpen) { this.props.onDrawerOpen(); } - }, + }; - _onDrawerClose: function() { + _onDrawerClose = () => { if (this.props.onDrawerClose) { this.props.onDrawerClose(); } - }, + }; - _onDrawerStateChanged: function(event) { + _onDrawerStateChanged = (event: DrawerStateEvent) => { if (this.props.onDrawerStateChanged) { this.props.onDrawerStateChanged( DRAWER_STATES[event.nativeEvent.drawerState], ); } - }, + }; /** * Opens the drawer. */ - openDrawer: function() { + openDrawer() { UIManager.dispatchViewManagerCommand( this._getDrawerLayoutHandle(), - UIManager.AndroidDrawerLayout.Commands.openDrawer, + UIManager.getViewManagerConfig('AndroidDrawerLayout').Commands.openDrawer, null, ); - }, + } /** * Closes the drawer. */ - closeDrawer: function() { + closeDrawer() { UIManager.dispatchViewManagerCommand( this._getDrawerLayoutHandle(), - UIManager.AndroidDrawerLayout.Commands.closeDrawer, + UIManager.getViewManagerConfig('AndroidDrawerLayout').Commands + .closeDrawer, null, ); - }, + } + /** * Closing and opening example * Note: To access the drawer you have to give it a ref. Refs do not work on stateless components @@ -285,10 +302,63 @@ const DrawerLayoutAndroid = createReactClass({ * ) * } */ - _getDrawerLayoutHandle: function() { - return ReactNative.findNodeHandle(this.refs[RK_DRAWER_REF]); - }, -}); + _getDrawerLayoutHandle() { + return ReactNative.findNodeHandle(this._nativeRef.current); + } + + /** + * Native methods + */ + blur() { + /* $FlowFixMe(>=0.87.0 site=react_native_android_fb) This comment + * suppresses an error found when Flow v0.87 was deployed. To see the + * error, delete this comment and run Flow. */ + nullthrows(this._nativeRef.current).blur(); + } + + focus() { + /* $FlowFixMe(>=0.87.0 site=react_native_android_fb) This comment + * suppresses an error found when Flow v0.87 was deployed. To see the + * error, delete this comment and run Flow. */ + nullthrows(this._nativeRef.current).focus(); + } + + measure(callback: MeasureOnSuccessCallback) { + /* $FlowFixMe(>=0.87.0 site=react_native_android_fb) This comment + * suppresses an error found when Flow v0.87 was deployed. To see the + * error, delete this comment and run Flow. */ + nullthrows(this._nativeRef.current).measure(callback); + } + + measureInWindow(callback: MeasureInWindowOnSuccessCallback) { + /* $FlowFixMe(>=0.87.0 site=react_native_android_fb) This comment + * suppresses an error found when Flow v0.87 was deployed. To see the + * error, delete this comment and run Flow. */ + nullthrows(this._nativeRef.current).measureInWindow(callback); + } + + measureLayout( + relativeToNativeNode: number, + onSuccess: MeasureLayoutOnSuccessCallback, + onFail?: () => void, + ) { + /* $FlowFixMe(>=0.87.0 site=react_native_android_fb) This comment + * suppresses an error found when Flow v0.87 was deployed. To see the + * error, delete this comment and run Flow. */ + nullthrows(this._nativeRef.current).measureLayout( + relativeToNativeNode, + onSuccess, + onFail, + ); + } + + setNativeProps(nativeProps: Object) { + /* $FlowFixMe(>=0.87.0 site=react_native_android_fb) This comment + * suppresses an error found when Flow v0.87 was deployed. To see the + * error, delete this comment and run Flow. */ + nullthrows(this._nativeRef.current).setNativeProps(nativeProps); + } +} const styles = StyleSheet.create({ base: { @@ -320,7 +390,4 @@ const styles = StyleSheet.create({ }, }); -// The View that contains both the actual drawer and the main view -const AndroidDrawerLayout = requireNativeComponent('AndroidDrawerLayout'); - module.exports = DrawerLayoutAndroid; diff --git a/Libraries/Components/DrawerAndroid/DrawerLayoutAndroid.ios.js b/Libraries/Components/DrawerAndroid/DrawerLayoutAndroid.ios.js index 260d559929796f..7a00b6636cf664 100644 --- a/Libraries/Components/DrawerAndroid/DrawerLayoutAndroid.ios.js +++ b/Libraries/Components/DrawerAndroid/DrawerLayoutAndroid.ios.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/Components/Keyboard/Keyboard.js b/Libraries/Components/Keyboard/Keyboard.js index feeb4a4ec2ce70..3a89f8b6101eb6 100644 --- a/Libraries/Components/Keyboard/Keyboard.js +++ b/Libraries/Components/Keyboard/Keyboard.js @@ -1,23 +1,23 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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 + * @flow strict-local */ 'use strict'; const LayoutAnimation = require('LayoutAnimation'); -const invariant = require('fbjs/lib/invariant'); +const invariant = require('invariant'); const NativeEventEmitter = require('NativeEventEmitter'); const KeyboardObserver = require('NativeModules').KeyboardObserver; const dismissKeyboard = require('dismissKeyboard'); const KeyboardEventEmitter = new NativeEventEmitter(KeyboardObserver); -type KeyboardEventName = +export type KeyboardEventName = | 'keyboardWillShow' | 'keyboardDidShow' | 'keyboardWillHide' @@ -25,6 +25,13 @@ type KeyboardEventName = | 'keyboardWillChangeFrame' | 'keyboardDidChangeFrame'; +export type KeyboardEventEasing = + | 'easeIn' + | 'easeInEaseOut' + | 'easeOut' + | 'linear' + | 'keyboard'; + type ScreenRect = $ReadOnly<{| screenX: number, screenY: number, @@ -33,10 +40,11 @@ type ScreenRect = $ReadOnly<{| |}>; export type KeyboardEvent = $ReadOnly<{| - duration?: number, - easing?: string, + duration: number, + easing: KeyboardEventEasing, endCoordinates: ScreenRect, - startCoordinates?: ScreenRect, + startCoordinates: ScreenRect, + isEventFromThisApp: boolean, |}>; type KeyboardEventListener = (e: KeyboardEvent) => void; @@ -121,7 +129,10 @@ let Keyboard = { * @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. */ - removeListener(eventName: KeyboardEventName, callback: Function) { + removeListener( + eventName: KeyboardEventName, + callback: KeyboardEventListener, + ) { invariant(false, 'Dummy method used for documentation'); }, @@ -155,12 +166,12 @@ Keyboard = KeyboardEventEmitter; Keyboard.dismiss = dismissKeyboard; Keyboard.scheduleLayoutAnimation = function(event: KeyboardEvent) { const {duration, easing} = event; - if (duration) { + if (duration != null && duration !== 0) { LayoutAnimation.configureNext({ duration: duration, update: { duration: duration, - type: (easing && LayoutAnimation.Types[easing]) || 'keyboard', + type: (easing != null && LayoutAnimation.Types[easing]) || 'keyboard', }, }); } diff --git a/Libraries/Components/Keyboard/KeyboardAvoidingView.js b/Libraries/Components/Keyboard/KeyboardAvoidingView.js index 4973fd4a0a3805..b8b123c86bb2fa 100644 --- a/Libraries/Components/Keyboard/KeyboardAvoidingView.js +++ b/Libraries/Components/Keyboard/KeyboardAvoidingView.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -52,8 +52,6 @@ type State = {| bottom: number, |}; -const viewRef = 'VIEW'; - /** * View that moves out of the way when the keyboard appears by automatically * adjusting its height, position, or bottom padding. @@ -66,10 +64,13 @@ class KeyboardAvoidingView extends React.Component { _frame: ?ViewLayout = null; _subscriptions: Array = []; + viewRef: {current: React.ElementRef | null}; - state = { - bottom: 0, - }; + constructor(props: Props) { + super(props); + this.state = {bottom: 0}; + this.viewRef = React.createRef(); + } _relativeKeyboardHeight(keyboardFrame): number { const frame = this._frame; @@ -99,9 +100,10 @@ class KeyboardAvoidingView extends React.Component { if (duration && easing) { LayoutAnimation.configureNext({ - duration: duration, + // We have to pass the duration equal to minimal accepted duration defined here: RCTLayoutAnimation.m + duration: duration > 10 ? duration : 10, update: { - duration: duration, + duration: duration > 10 ? duration : 10, type: LayoutAnimation.Types[easing] || 'keyboard', }, }); @@ -150,7 +152,7 @@ class KeyboardAvoidingView extends React.Component { children, contentContainerStyle, enabled, - keyboardVerticalOffset, // eslint-disable-line no-unused-vars + keyboardVerticalOffset, style, ...props } = this.props; @@ -158,7 +160,7 @@ class KeyboardAvoidingView extends React.Component { switch (behavior) { case 'height': let heightStyle; - if (this._frame != null) { + if (this._frame != null && this.state.bottom > 0) { // Note that we only apply a height change when there is keyboard present, // i.e. this.state.bottom is greater than 0. If we remove that condition, // this.frame.height will never go back to its original value. @@ -170,7 +172,7 @@ class KeyboardAvoidingView extends React.Component { } return ( { case 'position': return ( @@ -203,7 +205,7 @@ class KeyboardAvoidingView extends React.Component { case 'padding': return ( { default: return ( diff --git a/Libraries/Components/Keyboard/__tests__/Keyboard-test.js b/Libraries/Components/Keyboard/__tests__/Keyboard-test.js new file mode 100644 index 00000000000000..98f8b363521196 --- /dev/null +++ b/Libraries/Components/Keyboard/__tests__/Keyboard-test.js @@ -0,0 +1,93 @@ +/** + * 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 + * @emails oncall+react_native + */ + +'use strict'; + +const Keyboard = require('Keyboard'); +const dismissKeyboard = require('dismissKeyboard'); +const LayoutAnimation = require('LayoutAnimation'); + +const NativeEventEmitter = require('NativeEventEmitter'); +const NativeModules = require('NativeModules'); + +jest.mock('LayoutAnimation'); + +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); + expect(Keyboard._nativeModule).toBe(KeyboardEventEmitter._nativeModule); + }); + + it('uses dismissKeyboard utility', () => { + expect(Keyboard.dismiss).toBe(dismissKeyboard); + }); + + describe('scheduling layout animation', () => { + const scheduleLayoutAnimation = ( + duration: number | null, + easing: string | null, + ): void => Keyboard.scheduleLayoutAnimation({duration, easing}); + + it('triggers layout animation', () => { + scheduleLayoutAnimation(12, 'spring'); + expect(LayoutAnimation.configureNext).toHaveBeenCalledWith({ + duration: 12, + update: { + duration: 12, + type: 'spring', + }, + }); + }); + + it('does not trigger animation when duration is null', () => { + scheduleLayoutAnimation(null, 'spring'); + expect(LayoutAnimation.configureNext).not.toHaveBeenCalled(); + }); + + it('does not trigger animation when duration is 0', () => { + scheduleLayoutAnimation(0, 'spring'); + expect(LayoutAnimation.configureNext).not.toHaveBeenCalled(); + }); + + describe('animation update type', () => { + const assertAnimationUpdateType = type => + expect(LayoutAnimation.configureNext).toHaveBeenCalledWith( + expect.objectContaining({ + duration: expect.anything(), + update: {duration: expect.anything(), type}, + }), + ); + + it('retrieves type from LayoutAnimation', () => { + scheduleLayoutAnimation(12, 'linear'); + assertAnimationUpdateType('linear'); + }); + + it("defaults to 'keyboard' when key in LayoutAnimation is not found", () => { + scheduleLayoutAnimation(12, 'some-unknown-animation-type'); + assertAnimationUpdateType('keyboard'); + }); + + it("defaults to 'keyboard' when easing is null", () => { + scheduleLayoutAnimation(12, null); + assertAnimationUpdateType('keyboard'); + }); + }); + }); +}); diff --git a/Libraries/Components/MaskedView/MaskedViewIOS.android.js b/Libraries/Components/MaskedView/MaskedViewIOS.android.js index 1e779382f6763f..95d93f156b29cd 100644 --- a/Libraries/Components/MaskedView/MaskedViewIOS.android.js +++ b/Libraries/Components/MaskedView/MaskedViewIOS.android.js @@ -1,11 +1,11 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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 + * @flow */ 'use strict'; diff --git a/Libraries/Components/MaskedView/MaskedViewIOS.ios.js b/Libraries/Components/MaskedView/MaskedViewIOS.ios.js index c65822a64b2d30..b8af448da8dc2b 100644 --- a/Libraries/Components/MaskedView/MaskedViewIOS.ios.js +++ b/Libraries/Components/MaskedView/MaskedViewIOS.ios.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -8,27 +8,23 @@ * @flow */ -const PropTypes = require('prop-types'); const React = require('React'); const StyleSheet = require('StyleSheet'); const View = require('View'); -const ViewPropTypes = require('ViewPropTypes'); -const requireNativeComponent = require('requireNativeComponent'); +const RCTMaskedViewNativeComponent = require('RCTMaskedViewNativeComponent'); import type {ViewProps} from 'ViewPropTypes'; -const RCTMaskedView = requireNativeComponent('RCTMaskedView'); - -type Props = { +type Props = $ReadOnly<{| ...ViewProps, - children: any, + children: React.Node, /** * Should be a React element to be rendered and applied as the * mask for the child element. */ maskElement: React.Element, -}; +|}>; /** * Renders the child view with a mask specified in the `maskElement` prop. @@ -67,11 +63,6 @@ type Props = { * */ class MaskedViewIOS extends React.Component { - static propTypes = { - ...ViewPropTypes, - maskElement: PropTypes.element.isRequired, - }; - _hasWarnedInvalidRenderMask = false; render() { @@ -89,12 +80,12 @@ class MaskedViewIOS extends React.Component { } return ( - + {maskElement} {children} - + ); } } diff --git a/Libraries/Components/MaskedView/RCTMaskedViewNativeComponent.js b/Libraries/Components/MaskedView/RCTMaskedViewNativeComponent.js new file mode 100644 index 00000000000000..b2cb472f47b110 --- /dev/null +++ b/Libraries/Components/MaskedView/RCTMaskedViewNativeComponent.js @@ -0,0 +1,24 @@ +/** + * 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 + */ + +const requireNativeComponent = require('requireNativeComponent'); + +import type {ViewProps} from 'ViewPropTypes'; +import type {NativeComponent} from 'ReactNative'; + +type NativeProps = $ReadOnly<{| + ...ViewProps, +|}>; + +type RCTMaskedViewNativeType = Class>; + +module.exports = ((requireNativeComponent( + 'RCTMaskedView', +): any): RCTMaskedViewNativeType); diff --git a/Libraries/Components/Navigation/NavigatorIOS.android.js b/Libraries/Components/Navigation/NavigatorIOS.android.js deleted file mode 100644 index 260d559929796f..00000000000000 --- a/Libraries/Components/Navigation/NavigatorIOS.android.js +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @format - */ - -'use strict'; - -module.exports = require('UnimplementedView'); diff --git a/Libraries/Components/Navigation/NavigatorIOS.ios.js b/Libraries/Components/Navigation/NavigatorIOS.ios.js deleted file mode 100644 index 8f28d79147fe77..00000000000000 --- a/Libraries/Components/Navigation/NavigatorIOS.ios.js +++ /dev/null @@ -1,932 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * - * 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 EventEmitter = require('EventEmitter'); -const Image = require('Image'); -const RCTNavigatorManager = require('NativeModules').NavigatorManager; -const React = require('React'); -const PropTypes = require('prop-types'); -const ReactNative = require('ReactNative'); -const StaticContainer = require('StaticContainer.react'); -const StyleSheet = require('StyleSheet'); -const TVEventHandler = require('TVEventHandler'); -const View = require('View'); -const ViewPropTypes = require('ViewPropTypes'); - -const createReactClass = require('create-react-class'); -const invariant = require('fbjs/lib/invariant'); -const requireNativeComponent = require('requireNativeComponent'); - -/* $FlowFixMe(>=0.54.0 site=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. */ -const keyMirror = require('fbjs/lib/keyMirror'); - -const TRANSITIONER_REF = 'transitionerRef'; - -let __uid = 0; -function getuid() { - return __uid++; -} - -class NavigatorTransitionerIOS extends React.Component<$FlowFixMeProps> { - requestSchedulingNavigation(cb) { - RCTNavigatorManager.requestSchedulingJavaScriptNavigation( - ReactNative.findNodeHandle(this), - cb, - ); - } - - render() { - return ; - } -} - -const SystemIconLabels = { - done: true, - cancel: true, - edit: true, - save: true, - add: true, - compose: true, - reply: true, - action: true, - organize: true, - bookmarks: true, - search: true, - refresh: true, - stop: true, - camera: true, - trash: true, - play: true, - pause: true, - rewind: true, - 'fast-forward': true, - undo: true, - redo: true, - 'page-curl': true, -}; -const SystemIcons = keyMirror(SystemIconLabels); - -type SystemButtonType = $Enum; - -type Route = { - component: Function, - title: string, - titleImage?: Object, - passProps?: Object, - backButtonTitle?: string, - backButtonIcon?: Object, - leftButtonTitle?: string, - leftButtonIcon?: Object, - leftButtonSystemIcon?: SystemButtonType, - onLeftButtonPress?: Function, - rightButtonTitle?: string, - rightButtonIcon?: Object, - rightButtonSystemIcon?: SystemButtonType, - onRightButtonPress?: Function, - wrapperStyle?: any, -}; - -type State = { - idStack: Array, - routeStack: Array, - requestedTopOfStack: number, - observedTopOfStack: number, - progress: number, - fromIndex: number, - toIndex: number, - makingNavigatorRequest: boolean, - updatingAllIndicesAtOrBeyond: ?number, -}; - -type Event = Object; - -/** - * Think of `` as simply a component that renders an - * `RCTNavigator`, and moves the `RCTNavigator`'s `requestedTopOfStack` pointer - * forward and backward. The `RCTNavigator` interprets changes in - * `requestedTopOfStack` to be pushes and pops of children that are rendered. - * `` always ensures that whenever the `requestedTopOfStack` - * pointer is moved, that we've also rendered enough children so that the - * `RCTNavigator` can carry out the push/pop with those children. - * `` also removes children that will no longer be needed - * (after the pop of a child has been fully completed/animated out). - */ - -/** - * `NavigatorIOS` is a wrapper around - * [`UINavigationController`](https://developer.apple.com/library/ios/documentation/UIKit/Reference/UINavigationController_Class/), - * enabling you to implement a navigation stack. It works exactly the same as it - * would on a native app using `UINavigationController`, providing the same - * animations and behavior from UIKit. - * - * As the name implies, it is only available on iOS. Take a look at - * [`React Navigation`](https://reactnavigation.org/) for a cross-platform - * solution in JavaScript, or check out either of these components for native - * solutions: [native-navigation](http://airbnb.io/native-navigation/), - * [react-native-navigation](https://github.com/wix/react-native-navigation). - * - * To set up the navigator, provide the `initialRoute` prop with a route - * object. A route object is used to describe each scene that your app - * navigates to. `initialRoute` represents the first route in your navigator. - * - * ``` - * import PropTypes from 'prop-types'; - * import React, { Component } from 'react'; - * import { NavigatorIOS, Text } from 'react-native'; - * - * export default class NavigatorIOSApp extends Component { - * render() { - * return ( - * - * ); - * } - * } - * - * class MyScene extends Component { - * static propTypes = { - * title: PropTypes.string.isRequired, - * navigator: PropTypes.object.isRequired, - * } - * - * _onForward = () => { - * this.props.navigator.push({ - * title: 'Scene ' + nextIndex, - * }); - * } - * - * render() { - * return ( - * - * Current Scene: { this.props.title } - * - * Tap me to load the next scene - * - * - * ) - * } - * } - * ``` - * - * In this code, the navigator renders the component specified in initialRoute, - * which in this case is `MyScene`. This component will receive a `route` prop - * and a `navigator` prop representing the navigator. The navigator's navigation - * bar will render the title for the current scene, "My Initial Scene". - * - * You can optionally pass in a `passProps` property to your `initialRoute`. - * `NavigatorIOS` passes this in as props to the rendered component: - * - * ``` - * initialRoute={{ - * component: MyScene, - * title: 'My Initial Scene', - * passProps: { myProp: 'foo' } - * }} - * ``` - * - * You can then access the props passed in via `{this.props.myProp}`. - * - * #### Handling Navigation - * - * To trigger navigation functionality such as pushing or popping a view, you - * have access to a `navigator` object. The object is passed in as a prop to any - * component that is rendered by `NavigatorIOS`. You can then call the - * relevant methods to perform the navigation action you need: - * - * ``` - * class MyView extends Component { - * _handleBackPress() { - * this.props.navigator.pop(); - * } - * - * _handleNextPress(nextRoute) { - * this.props.navigator.push(nextRoute); - * } - * - * render() { - * const nextRoute = { - * component: MyView, - * title: 'Bar That', - * passProps: { myProp: 'bar' } - * }; - * return( - * this._handleNextPress(nextRoute)}> - * - * See you on the other nav {this.props.myProp}! - * - * - * ); - * } - * } - * ``` - * - * You can also trigger navigator functionality from the `NavigatorIOS` - * component: - * - * ``` - * class NavvyIOS extends Component { - * _handleNavigationRequest() { - * this.refs.nav.push({ - * component: MyView, - * title: 'Genius', - * passProps: { myProp: 'genius' }, - * }); - * } - * - * render() { - * return ( - * this._handleNavigationRequest(), - * }} - * style={{flex: 1}} - * /> - * ); - * } - * } - * ``` - * - * The code above adds a `_handleNavigationRequest` private method that is - * invoked from the `NavigatorIOS` component when the right navigation bar item - * is pressed. To get access to the navigator functionality, a reference to it - * is saved in the `ref` prop and later referenced to push a new scene into the - * navigation stack. - * - * #### Navigation Bar Configuration - * - * Props passed to `NavigatorIOS` will set the default configuration - * for the navigation bar. Props passed as properties to a route object will set - * the configuration for that route's navigation bar, overriding any props - * passed to the `NavigatorIOS` component. - * - * ``` - * _handleNavigationRequest() { - * this.refs.nav.push({ - * //... - * passProps: { myProp: 'genius' }, - * barTintColor: '#996699', - * }); - * } - * - * render() { - * return ( - * - * ); - * } - * ``` - * - * In the example above the navigation bar color is changed when the new route - * is pushed. - * - */ -const NavigatorIOS = createReactClass({ - displayName: 'NavigatorIOS', - - propTypes: { - /** - * NavigatorIOS uses `route` objects to identify child views, their props, - * and navigation bar configuration. Navigation operations such as push - * operations expect routes to look like this the `initialRoute`. - */ - initialRoute: PropTypes.shape({ - /** - * The React Class to render for this route - */ - component: PropTypes.func.isRequired, - - /** - * The title displayed in the navigation bar and the back button for this - * route. - */ - title: PropTypes.string.isRequired, - - /** - * If set, a title image will appear instead of the text title. - */ - titleImage: Image.propTypes.source, - - /** - * Use this to specify additional props to pass to the rendered - * component. `NavigatorIOS` will automatically pass in `route` and - * `navigator` props to the component. - */ - passProps: PropTypes.object, - - /** - * If set, the left navigation button image will be displayed using this - * source. Note that this doesn't apply to the header of the current - * view, but to those views that are subsequently pushed. - */ - backButtonIcon: Image.propTypes.source, - - /** - * If set, the left navigation button text will be set to this. Note that - * this doesn't apply to the left button of the current view, but to - * those views that are subsequently pushed - */ - backButtonTitle: PropTypes.string, - - /** - * If set, the left navigation button image will be displayed using - * this source. - */ - leftButtonIcon: Image.propTypes.source, - - /** - * If set, the left navigation button will display this text. - */ - leftButtonTitle: PropTypes.string, - - /** - * If set, the left header button will appear with this system icon - * - * Supported icons are `done`, `cancel`, `edit`, `save`, `add`, - * `compose`, `reply`, `action`, `organize`, `bookmarks`, `search`, - * `refresh`, `stop`, `camera`, `trash`, `play`, `pause`, `rewind`, - * `fast-forward`, `undo`, `redo`, and `page-curl` - */ - leftButtonSystemIcon: PropTypes.oneOf(Object.keys(SystemIcons)), - - /** - * This function will be invoked when the left navigation bar item is - * pressed. - */ - onLeftButtonPress: PropTypes.func, - - /** - * If set, the right navigation button image will be displayed using - * this source. - */ - rightButtonIcon: Image.propTypes.source, - - /** - * If set, the right navigation button will display this text. - */ - rightButtonTitle: PropTypes.string, - - /** - * If set, the right header button will appear with this system icon - * - * See leftButtonSystemIcon for supported icons - */ - rightButtonSystemIcon: PropTypes.oneOf(Object.keys(SystemIcons)), - - /** - * This function will be invoked when the right navigation bar item is - * pressed. - */ - onRightButtonPress: PropTypes.func, - - /** - * Styles for the navigation item containing the component. - */ - wrapperStyle: ViewPropTypes.style, - - /** - * Boolean value that indicates whether the navigation bar is hidden. - */ - navigationBarHidden: PropTypes.bool, - - /** - * Boolean value that indicates whether to hide the 1px hairline - * shadow. - */ - shadowHidden: PropTypes.bool, - - /** - * The color used for the buttons in the navigation bar. - */ - tintColor: PropTypes.string, - - /** - * The background color of the navigation bar. - */ - barTintColor: PropTypes.string, - - /** - * The style of the navigation bar. Supported values are 'default', 'black'. - * Use 'black' instead of setting `barTintColor` to black. This produces - * a navigation bar with the native iOS style with higher translucency. - */ - barStyle: PropTypes.oneOf(['default', 'black']), - - /** - * The text color of the navigation bar title. - */ - titleTextColor: PropTypes.string, - - /** - * Boolean value that indicates whether the navigation bar is - * translucent. - */ - translucent: PropTypes.bool, - }).isRequired, - - /** - * Boolean value that indicates whether the navigation bar is hidden - * by default. - */ - navigationBarHidden: PropTypes.bool, - - /** - * Boolean value that indicates whether to hide the 1px hairline shadow - * by default. - */ - shadowHidden: PropTypes.bool, - - /** - * The default wrapper style for components in the navigator. - * A common use case is to set the `backgroundColor` for every scene. - */ - itemWrapperStyle: ViewPropTypes.style, - - /** - * The default color used for the buttons in the navigation bar. - */ - tintColor: PropTypes.string, - - /** - * The default background color of the navigation bar. - */ - barTintColor: PropTypes.string, - - /** - * The style of the navigation bar. Supported values are 'default', 'black'. - * Use 'black' instead of setting `barTintColor` to black. This produces - * a navigation bar with the native iOS style with higher translucency. - */ - barStyle: PropTypes.oneOf(['default', 'black']), - - /** - * The default text color of the navigation bar title. - */ - titleTextColor: PropTypes.string, - - /** - * Boolean value that indicates whether the navigation bar is - * translucent by default - */ - translucent: PropTypes.bool, - - /** - * Boolean value that indicates whether the interactive pop gesture is - * enabled. This is useful for enabling/disabling the back swipe navigation - * gesture. - * - * If this prop is not provided, the default behavior is for the back swipe - * gesture to be enabled when the navigation bar is shown and disabled when - * the navigation bar is hidden. Once you've provided the - * `interactivePopGestureEnabled` prop, you can never restore the default - * behavior. - */ - interactivePopGestureEnabled: PropTypes.bool, - }, - - navigator: (undefined: ?Object), - - UNSAFE_componentWillMount: function() { - // Precompute a pack of callbacks that's frequently generated and passed to - // instances. - this.navigator = { - push: this.push, - pop: this.pop, - popN: this.popN, - replace: this.replace, - replaceAtIndex: this.replaceAtIndex, - replacePrevious: this.replacePrevious, - replacePreviousAndPop: this.replacePreviousAndPop, - resetTo: this.resetTo, - popToRoute: this.popToRoute, - popToTop: this.popToTop, - }; - }, - - componentDidMount: function() { - this._enableTVEventHandler(); - }, - - componentWillUnmount: function() { - this._disableTVEventHandler(); - }, - - getDefaultProps: function(): Object { - return { - translucent: true, - }; - }, - - getInitialState: function(): State { - return { - idStack: [getuid()], - routeStack: [this.props.initialRoute], - // The navigation index that we wish to push/pop to. - requestedTopOfStack: 0, - // The last index that native has sent confirmation of completed push/pop - // for. At this point, we can discard any views that are beyond the - // `requestedTopOfStack`. A value of `null` means we have not received - // any confirmation, ever. We may receive an `observedTopOfStack` without - // ever requesting it - native can instigate pops of its own with the - // backswipe gesture. - observedTopOfStack: 0, - progress: 1, - fromIndex: 0, - toIndex: 0, - // Whether or not we are making a navigator request to push/pop. (Used - // for performance optimization). - makingNavigatorRequest: false, - // Whether or not we are updating children of navigator and if so (not - // `null`) which index marks the beginning of all updates. Used for - // performance optimization. - updatingAllIndicesAtOrBeyond: 0, - }; - }, - - _toFocusOnNavigationComplete: (undefined: any), - - _handleFocusRequest: function(item: any) { - if (this.state.makingNavigatorRequest) { - this._toFocusOnNavigationComplete = item; - } else { - this._getFocusEmitter().emit('focus', item); - } - }, - - _focusEmitter: (undefined: ?EventEmitter), - - _getFocusEmitter: function(): EventEmitter { - // Flow not yet tracking assignments to instance fields. - let focusEmitter = this._focusEmitter; - if (!focusEmitter) { - focusEmitter = new EventEmitter(); - this._focusEmitter = focusEmitter; - } - return focusEmitter; - }, - - getChildContext: function(): { - onFocusRequested: Function, - focusEmitter: EventEmitter, - } { - return { - onFocusRequested: this._handleFocusRequest, - focusEmitter: this._getFocusEmitter(), - }; - }, - - childContextTypes: { - onFocusRequested: PropTypes.func, - focusEmitter: PropTypes.instanceOf(EventEmitter), - }, - - _tryLockNavigator: function(cb: () => void) { - this.refs[TRANSITIONER_REF].requestSchedulingNavigation( - acquiredLock => acquiredLock && cb(), - ); - }, - - _handleNavigatorStackChanged: function(e: Event) { - const newObservedTopOfStack = e.nativeEvent.stackLength - 1; - - invariant( - newObservedTopOfStack <= this.state.requestedTopOfStack, - 'No navigator item should be pushed without JS knowing about it %s %s', - newObservedTopOfStack, - this.state.requestedTopOfStack, - ); - const wasWaitingForConfirmation = - this.state.requestedTopOfStack !== this.state.observedTopOfStack; - if (wasWaitingForConfirmation) { - invariant( - newObservedTopOfStack === this.state.requestedTopOfStack, - 'If waiting for observedTopOfStack to reach requestedTopOfStack, ' + - 'the only valid observedTopOfStack should be requestedTopOfStack.', - ); - } - // Mark the most recent observation regardless of if we can lock the - // navigator. `observedTopOfStack` merely represents what we've observed - // and this first `setState` is only executed to update debugging - // overlays/navigation bar. - // Also reset progress, toIndex, and fromIndex as they might not end - // in the correct states for a two possible reasons: - // Progress isn't always 0 or 1 at the end, the system rounds - // If the Navigator is offscreen these values won't be updated - // TOOD: Revisit this decision when no longer relying on native navigator. - const nextState = { - observedTopOfStack: newObservedTopOfStack, - makingNavigatorRequest: false, - updatingAllIndicesAtOrBeyond: null, - progress: 1, - toIndex: newObservedTopOfStack, - fromIndex: newObservedTopOfStack, - }; - this.setState(nextState, this._eliminateUnneededChildren); - }, - - _eliminateUnneededChildren: function() { - // Updating the indices that we're deleting and that's all. (Truth: Nothing - // even uses the indices in this case, but let's make this describe the - // truth anyways). - const updatingAllIndicesAtOrBeyond = - this.state.routeStack.length > this.state.observedTopOfStack + 1 - ? this.state.observedTopOfStack + 1 - : null; - this.setState({ - idStack: this.state.idStack.slice(0, this.state.observedTopOfStack + 1), - routeStack: this.state.routeStack.slice( - 0, - this.state.observedTopOfStack + 1, - ), - // Now we rerequest the top of stack that we observed. - requestedTopOfStack: this.state.observedTopOfStack, - makingNavigatorRequest: true, - updatingAllIndicesAtOrBeyond: updatingAllIndicesAtOrBeyond, - }); - }, - - /** - * Navigate forward to a new route. - * @param route The new route to navigate to. - */ - push: function(route: Route) { - invariant(!!route, 'Must supply route to push'); - // Make sure all previous requests are caught up first. Otherwise reject. - if (this.state.requestedTopOfStack === this.state.observedTopOfStack) { - this._tryLockNavigator(() => { - const nextStack = this.state.routeStack.concat([route]); - const nextIDStack = this.state.idStack.concat([getuid()]); - this.setState({ - // We have to make sure that we've also supplied enough views to - // satisfy our request to adjust the `requestedTopOfStack`. - idStack: nextIDStack, - routeStack: nextStack, - requestedTopOfStack: nextStack.length - 1, - makingNavigatorRequest: true, - updatingAllIndicesAtOrBeyond: nextStack.length - 1, - }); - }); - } - }, - - /** - * Go back N scenes at once. When N=1, behavior matches `pop()`. - * @param n The number of scenes to pop. - */ - popN: function(n: number) { - if (n === 0) { - return; - } - // Make sure all previous requests are caught up first. Otherwise reject. - if (this.state.requestedTopOfStack === this.state.observedTopOfStack) { - if (this.state.requestedTopOfStack > 0) { - this._tryLockNavigator(() => { - const newRequestedTopOfStack = this.state.requestedTopOfStack - n; - invariant(newRequestedTopOfStack >= 0, 'Cannot pop below 0'); - this.setState({ - requestedTopOfStack: newRequestedTopOfStack, - makingNavigatorRequest: true, - updatingAllIndicesAtOrBeyond: this.state.requestedTopOfStack - n, - }); - }); - } - } - }, - - /** - * Pop back to the previous scene. - */ - pop: function() { - this.popN(1); - }, - - /** - * Replace a route in the navigation stack. - * - * @param route The new route that will replace the specified one. - * @param index The route into the stack that should be replaced. - * If it is negative, it counts from the back of the stack. - */ - replaceAtIndex: function(route: Route, index: number) { - invariant(!!route, 'Must supply route to replace'); - if (index < 0) { - index += this.state.routeStack.length; - } - - if (this.state.routeStack.length <= index) { - return; - } - - // I don't believe we need to lock for a replace since there's no - // navigation actually happening - const nextIDStack = this.state.idStack.slice(); - const nextRouteStack = this.state.routeStack.slice(); - nextIDStack[index] = getuid(); - nextRouteStack[index] = route; - - this.setState({ - idStack: nextIDStack, - routeStack: nextRouteStack, - makingNavigatorRequest: false, - updatingAllIndicesAtOrBeyond: index, - }); - }, - - /** - * Replace the route for the current scene and immediately - * load the view for the new route. - * @param route The new route to navigate to. - */ - replace: function(route: Route) { - this.replaceAtIndex(route, -1); - }, - - /** - * Replace the route/view for the previous scene. - * @param route The new route to will replace the previous scene. - */ - replacePrevious: function(route: Route) { - this.replaceAtIndex(route, -2); - }, - - /** - * Go back to the topmost item in the navigation stack. - */ - popToTop: function() { - this.popToRoute(this.state.routeStack[0]); - }, - - /** - * Go back to the item for a particular route object. - * @param route The new route to navigate to. - */ - popToRoute: function(route: Route) { - const indexOfRoute = this.state.routeStack.indexOf(route); - invariant( - indexOfRoute !== -1, - "Calling pop to route for a route that doesn't exist!", - ); - const numToPop = this.state.routeStack.length - indexOfRoute - 1; - this.popN(numToPop); - }, - - /** - * Replaces the previous route/view and transitions back to it. - * @param route The new route that replaces the previous scene. - */ - replacePreviousAndPop: function(route: Route) { - // Make sure all previous requests are caught up first. Otherwise reject. - if (this.state.requestedTopOfStack !== this.state.observedTopOfStack) { - return; - } - if (this.state.routeStack.length < 2) { - return; - } - this._tryLockNavigator(() => { - this.replacePrevious(route); - this.setState({ - requestedTopOfStack: this.state.requestedTopOfStack - 1, - makingNavigatorRequest: true, - }); - }); - }, - - /** - * Replaces the top item and pop to it. - * @param route The new route that will replace the topmost item. - */ - resetTo: function(route: Route) { - invariant(!!route, 'Must supply route to push'); - // Make sure all previous requests are caught up first. Otherwise reject. - if (this.state.requestedTopOfStack !== this.state.observedTopOfStack) { - return; - } - this.replaceAtIndex(route, 0); - this.popToRoute(route); - }, - - _handleNavigationComplete: function(e: Event) { - // Don't propagate to other NavigatorIOS instances this is nested in: - e.stopPropagation(); - - if (this._toFocusOnNavigationComplete) { - this._getFocusEmitter().emit('focus', this._toFocusOnNavigationComplete); - this._toFocusOnNavigationComplete = null; - } - this._handleNavigatorStackChanged(e); - }, - - _routeToStackItem: function(routeArg: Route, i: number) { - const {component, wrapperStyle, passProps, ...route} = routeArg; - const {itemWrapperStyle, ...props} = this.props; - const shouldUpdateChild = - this.state.updatingAllIndicesAtOrBeyond != null && - this.state.updatingAllIndicesAtOrBeyond >= i; - const Component = component; - return ( - - - - - - ); - }, - - _renderNavigationStackItems: function() { - const shouldRecurseToNavigator = - this.state.makingNavigatorRequest || - this.state.updatingAllIndicesAtOrBeyond !== null; - // If not recursing update to navigator at all, may as well avoid - // computation of navigator children. - const items = shouldRecurseToNavigator - ? this.state.routeStack.map(this._routeToStackItem) - : null; - return ( - - =0.41.0) - vertical={this.props.vertical} - requestedTopOfStack={this.state.requestedTopOfStack} - onNavigationComplete={this._handleNavigationComplete} - interactivePopGestureEnabled={ - this.props.interactivePopGestureEnabled - }> - {items} - - - ); - }, - - _tvEventHandler: (undefined: ?TVEventHandler), - - _enableTVEventHandler: function() { - this._tvEventHandler = new TVEventHandler(); - this._tvEventHandler.enable(this, function(cmp, evt) { - if (evt && evt.eventType === 'menu') { - cmp.pop(); - } - }); - }, - - _disableTVEventHandler: function() { - if (this._tvEventHandler) { - this._tvEventHandler.disable(); - delete this._tvEventHandler; - } - }, - - render: function() { - return ( - // $FlowFixMe(>=0.41.0) - {this._renderNavigationStackItems()} - ); - }, -}); - -const styles = StyleSheet.create({ - stackItem: { - backgroundColor: 'white', - overflow: 'hidden', - position: 'absolute', - top: 0, - left: 0, - right: 0, - bottom: 0, - }, - transitioner: { - flex: 1, - }, -}); - -const RCTNavigator = requireNativeComponent('RCTNavigator'); -const RCTNavigatorItem = requireNativeComponent('RCTNavItem'); - -module.exports = NavigatorIOS; diff --git a/Libraries/Components/Picker/AndroidDialogPickerNativeComponent.js b/Libraries/Components/Picker/AndroidDialogPickerNativeComponent.js new file mode 100644 index 00000000000000..89900c4fac7cfc --- /dev/null +++ b/Libraries/Components/Picker/AndroidDialogPickerNativeComponent.js @@ -0,0 +1,47 @@ +/** + * 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 requireNativeComponent = require('requireNativeComponent'); + +import type {SyntheticEvent} from 'CoreEventTypes'; +import type {TextStyleProp} from 'StyleSheet'; +import type {NativeComponent} from 'ReactNative'; + +type PickerAndroidChangeEvent = SyntheticEvent< + $ReadOnly<{| + position: number, + |}>, +>; + +type Item = $ReadOnly<{| + label: string, + value: ?(number | string), + color?: ?number, +|}>; + +type NativeProps = $ReadOnly<{| + enabled?: ?boolean, + items: $ReadOnlyArray, + mode?: ?('dialog' | 'dropdown'), + onSelect?: (event: PickerAndroidChangeEvent) => void, + selected: number, + prompt?: ?string, + testID?: string, + style?: ?TextStyleProp, + accessibilityLabel?: ?string, +|}>; + +type DialogPickerNativeType = Class>; + +module.exports = ((requireNativeComponent( + 'AndroidDialogPicker', +): any): DialogPickerNativeType); diff --git a/Libraries/Components/Picker/AndroidDropdownPickerNativeComponent.js b/Libraries/Components/Picker/AndroidDropdownPickerNativeComponent.js new file mode 100644 index 00000000000000..a39f6a223e16dd --- /dev/null +++ b/Libraries/Components/Picker/AndroidDropdownPickerNativeComponent.js @@ -0,0 +1,47 @@ +/** + * 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 requireNativeComponent = require('requireNativeComponent'); + +import type {SyntheticEvent} from 'CoreEventTypes'; +import type {TextStyleProp} from 'StyleSheet'; +import type {NativeComponent} from 'ReactNative'; + +type PickerAndroidChangeEvent = SyntheticEvent< + $ReadOnly<{| + position: number, + |}>, +>; + +type Item = $ReadOnly<{| + label: string, + value: ?(number | string), + color?: ?number, +|}>; + +type NativeProps = $ReadOnly<{| + enabled?: ?boolean, + items: $ReadOnlyArray, + mode?: ?('dialog' | 'dropdown'), + onSelect?: (event: PickerAndroidChangeEvent) => void, + selected: number, + prompt?: ?string, + testID?: string, + style?: ?TextStyleProp, + accessibilityLabel?: ?string, +|}>; + +type DropdownPickerNativeType = Class>; + +module.exports = ((requireNativeComponent( + 'AndroidDropdownPicker', +): any): DropdownPickerNativeType); diff --git a/Libraries/Components/Picker/Picker.js b/Libraries/Components/Picker/Picker.js index 1e3e96579e21cb..c96f18baf6fbad 100644 --- a/Libraries/Components/Picker/Picker.js +++ b/Libraries/Components/Picker/Picker.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -10,64 +10,103 @@ 'use strict'; -const ColorPropType = require('ColorPropType'); -const PickerIOS = require('PickerIOS'); const PickerAndroid = require('PickerAndroid'); +const PickerIOS = require('PickerIOS'); const Platform = require('Platform'); const React = require('React'); -const PropTypes = require('prop-types'); -const StyleSheetPropType = require('StyleSheetPropType'); -const TextStylePropTypes = require('TextStylePropTypes'); const UnimplementedView = require('UnimplementedView'); -const ViewPropTypes = require('ViewPropTypes'); -const ViewStylePropTypes = require('ViewStylePropTypes'); - -const itemStylePropType = StyleSheetPropType(TextStylePropTypes); -const pickerStyleType = StyleSheetPropType({ - ...ViewStylePropTypes, - color: ColorPropType, -}); +import type {TextStyleProp} from 'StyleSheet'; +import type {ColorValue} from 'StyleSheetTypes'; const MODE_DIALOG = 'dialog'; const MODE_DROPDOWN = 'dropdown'; -/** - * Individual selectable item in a Picker. - */ -class PickerItem extends React.Component<{ +type PickerItemProps = $ReadOnly<{| + /** + * Text to display for this item. + */ label: string, - value?: any, - color?: ColorPropType, + + /** + * The value to be passed to picker's `onValueChange` callback when + * this item is selected. Can be a string or an integer. + */ + value?: ?(number | string), + + /** + * Color of this item's text. + * @platform android + */ + color?: ColorValue, + + /** + * Used to locate the item in end-to-end tests. + */ testID?: string, -}> { - static propTypes = { - /** - * Text to display for this item. - */ - label: PropTypes.string.isRequired, - /** - * The value to be passed to picker's `onValueChange` callback when - * this item is selected. Can be a string or an integer. - */ - value: PropTypes.any, - /** - * Color of this item's text. - * @platform android - */ - color: ColorPropType, - /** - * Used to locate the item in end-to-end tests. - */ - testID: PropTypes.string, - }; +|}>; +/** + * Individual selectable item in a Picker. + */ +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. Can be a string or an integer. + */ + selectedValue?: ?(number | 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, + + /** + * 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, +|}>; + /** * Renders the native picker component on iOS and Android. Example: * @@ -78,16 +117,7 @@ class PickerItem extends React.Component<{ * * */ -class Picker extends React.Component<{ - style?: $FlowFixMe, - selectedValue?: any, - onValueChange?: Function, - enabled?: boolean, - mode?: 'dialog' | 'dropdown', - itemStyle?: $FlowFixMe, - prompt?: string, - testID?: string, -}> { +class Picker extends React.Component { /** * On Android, display the options in a dialog. */ @@ -104,58 +134,15 @@ class Picker extends React.Component<{ mode: MODE_DIALOG, }; - // $FlowFixMe(>=0.41.0) - static propTypes = { - ...ViewPropTypes, - style: pickerStyleType, - /** - * Value matching value of one of the items. Can be a string or an integer. - */ - selectedValue: PropTypes.any, - /** - * Callback for when an item is selected. This is called with the following parameters: - * - `itemValue`: the `value` prop of the item that was selected - * - `itemPosition`: the index of the selected item in this picker - */ - onValueChange: PropTypes.func, - /** - * If set to false, the picker will be disabled, i.e. the user will not be able to make a - * selection. - * @platform android - */ - enabled: PropTypes.bool, - /** - * 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: PropTypes.oneOf(['dialog', 'dropdown']), - /** - * Style to apply to each of the item labels. - * @platform ios - */ - itemStyle: itemStylePropType, - /** - * Prompt string for this picker, used on Android in dialog mode as the title of the dialog. - * @platform android - */ - prompt: PropTypes.string, - /** - * Used to locate this view in end-to-end tests. - */ - testID: PropTypes.string, - }; - render() { if (Platform.OS === 'ios') { - // $FlowFixMe found when converting React.createClass to ES6 + /* $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 found when converting React.createClass to ES6 + /* $FlowFixMe(>=0.81.0 site=react_native_android_fb) This suppression + * was added when renaming suppression sites. */ {this.props.children} ); } else { diff --git a/Libraries/Components/Picker/PickerAndroid.android.js b/Libraries/Components/Picker/PickerAndroid.android.js index 135301eff38893..af72f74459230b 100644 --- a/Libraries/Components/Picker/PickerAndroid.android.js +++ b/Libraries/Components/Picker/PickerAndroid.android.js @@ -1,81 +1,67 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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 + * @flow strict-local */ 'use strict'; -const ColorPropType = require('ColorPropType'); +const AndroidDropdownPickerNativeComponent = require('AndroidDropdownPickerNativeComponent'); +const AndroidDialogPickerNativeComponent = require('AndroidDialogPickerNativeComponent'); const React = require('React'); -const ReactPropTypes = require('prop-types'); const StyleSheet = require('StyleSheet'); -const StyleSheetPropType = require('StyleSheetPropType'); -const ViewPropTypes = require('ViewPropTypes'); -const ViewStylePropTypes = require('ViewStylePropTypes'); const processColor = require('processColor'); -const requireNativeComponent = require('requireNativeComponent'); - -const DropdownPicker = requireNativeComponent('AndroidDropdownPicker'); -const DialogPicker = requireNativeComponent('AndroidDialogPicker'); const REF_PICKER = 'picker'; const MODE_DROPDOWN = 'dropdown'; -const pickerStyleType = StyleSheetPropType({ - ...ViewStylePropTypes, - color: ColorPropType, -}); - -type Event = Object; +import type {SyntheticEvent} from 'CoreEventTypes'; +import type {TextStyleProp} from 'StyleSheet'; + +type PickerAndroidChangeEvent = SyntheticEvent< + $ReadOnly<{| + position: number, + |}>, +>; + +type PickerAndroidProps = $ReadOnly<{| + children?: React.Node, + style?: ?TextStyleProp, + selectedValue?: ?(number | string), + enabled?: ?boolean, + mode?: ?('dialog' | 'dropdown'), + onValueChange?: ?(itemValue: ?(string | number), itemIndex: number) => mixed, + prompt?: ?string, + testID?: string, +|}>; + +type Item = $ReadOnly<{| + label: string, + value: ?(number | string), + color?: ?number, +|}>; + +type PickerAndroidState = {| + selectedIndex: number, + items: $ReadOnlyArray, +|}; /** * Not exposed as a public API - use instead. */ + class PickerAndroid extends React.Component< - { - style?: $FlowFixMe, - selectedValue?: any, - enabled?: boolean, - mode?: 'dialog' | 'dropdown', - onValueChange?: Function, - prompt?: string, - testID?: string, - }, - *, + PickerAndroidProps, + PickerAndroidState, > { - static propTypes = { - ...ViewPropTypes, - style: pickerStyleType, - selectedValue: ReactPropTypes.any, - enabled: ReactPropTypes.bool, - mode: ReactPropTypes.oneOf(['dialog', 'dropdown']), - onValueChange: ReactPropTypes.func, - prompt: ReactPropTypes.string, - testID: ReactPropTypes.string, - }; - - constructor(props, context) { - super(props, context); - const state = this._stateFromProps(props); - - this.state = { - ...state, - initialSelectedIndex: state.selectedIndex, - }; - } - - UNSAFE_componentWillReceiveProps(nextProps) { - this.setState(this._stateFromProps(nextProps)); - } - - // Translate prop and children into stuff that the native picker understands. - _stateFromProps = props => { + static getDerivedStateFromProps( + props: PickerAndroidProps, + ): PickerAndroidState { let selectedIndex = 0; const items = React.Children.map(props.children, (child, index) => { if (child.props.value === props.selectedValue) { @@ -86,16 +72,22 @@ class PickerAndroid extends React.Component< label: child.props.label, }; if (child.props.color) { + /* $FlowFixMe(>=0.78.0 site=react_native_android_fb) This issue was + * found when making Flow check .android.js files. */ childProps.color = processColor(child.props.color); } return childProps; }); return {selectedIndex, items}; - }; + } + + state = PickerAndroid.getDerivedStateFromProps(this.props); render() { const Picker = - this.props.mode === MODE_DROPDOWN ? DropdownPicker : DialogPicker; + this.props.mode === MODE_DROPDOWN + ? AndroidDropdownPickerNativeComponent + : AndroidDialogPickerNativeComponent; const nativeProps = { enabled: this.props.enabled, @@ -103,35 +95,31 @@ class PickerAndroid extends React.Component< mode: this.props.mode, onSelect: this._onChange, prompt: this.props.prompt, - selected: this.state.initialSelectedIndex, + selected: this.state.selectedIndex, testID: this.props.testID, style: [styles.pickerAndroid, this.props.style], + /* $FlowFixMe(>=0.78.0 site=react_native_android_fb) This issue was found + * when making Flow check .android.js files. */ accessibilityLabel: this.props.accessibilityLabel, }; return ; } - _onChange = (event: Event) => { + _onChange = (event: PickerAndroidChangeEvent) => { if (this.props.onValueChange) { const position = event.nativeEvent.position; if (position >= 0) { const children = React.Children.toArray(this.props.children); const value = children[position].props.value; + /* $FlowFixMe(>=0.78.0 site=react_native_android_fb) This issue was + * found when making Flow check .android.js files. */ this.props.onValueChange(value, position); } else { this.props.onValueChange(null, position); } } - this._lastNativePosition = event.nativeEvent.position; - this.forceUpdate(); - }; - - componentDidMount() { - this._lastNativePosition = this.state.initialSelectedIndex; - } - componentDidUpdate() { // The picker is a controlled component. This means we expect the // on*Change handlers to be in charge of updating our // `selectedValue` prop. That way they can also @@ -140,14 +128,13 @@ class PickerAndroid extends React.Component< // truth, not the native component. if ( this.refs[REF_PICKER] && - this.state.selectedIndex !== this._lastNativePosition + this.state.selectedIndex !== event.nativeEvent.position ) { this.refs[REF_PICKER].setNativeProps({ selected: this.state.selectedIndex, }); - this._lastNativePosition = this.state.selectedIndex; } - } + }; } const styles = StyleSheet.create({ diff --git a/Libraries/Components/Picker/PickerAndroid.ios.js b/Libraries/Components/Picker/PickerAndroid.ios.js index 260d559929796f..7a00b6636cf664 100644 --- a/Libraries/Components/Picker/PickerAndroid.ios.js +++ b/Libraries/Components/Picker/PickerAndroid.ios.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/Components/Picker/PickerIOS.android.js b/Libraries/Components/Picker/PickerIOS.android.js index 36acfe18342b6f..006c5b4d1ae82f 100644 --- a/Libraries/Components/Picker/PickerIOS.android.js +++ b/Libraries/Components/Picker/PickerIOS.android.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/Components/Picker/PickerIOS.ios.js b/Libraries/Components/Picker/PickerIOS.ios.js index 62ebe53e89a3ea..1f47a53a97a11e 100644 --- a/Libraries/Components/Picker/PickerIOS.ios.js +++ b/Libraries/Components/Picker/PickerIOS.ios.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -18,7 +18,7 @@ const ReactNative = require('ReactNative'); const StyleSheet = require('StyleSheet'); const View = require('View'); const processColor = require('processColor'); -const requireNativeComponent = require('requireNativeComponent'); +const RCTPickerNativeComponent = require('RCTPickerNativeComponent'); import type {SyntheticEvent} from 'CoreEventTypes'; import type {ColorValue} from 'StyleSheetTypes'; @@ -27,14 +27,14 @@ import type {TextStyleProp} from 'StyleSheet'; type PickerIOSChangeEvent = SyntheticEvent< $ReadOnly<{| - newValue: any, + newValue: number | string, newIndex: number, |}>, >; type RCTPickerIOSItemType = $ReadOnly<{| label: ?Label, - value: ?any, + value: ?(number | string), textColor: ?number, |}>; @@ -47,14 +47,11 @@ type RCTPickerIOSType = Class< onStartShouldSetResponder: () => boolean, selectedIndex: number, style?: ?TextStyleProp, + testID?: ?string, |}>, >, >; -const RCTPickerIOS: RCTPickerIOSType = (requireNativeComponent( - 'RCTPicker', -): any); - type Label = Stringish | number; type Props = $ReadOnly<{| @@ -62,8 +59,8 @@ type Props = $ReadOnly<{| children: React.ChildrenArray>, itemStyle?: ?TextStyleProp, onChange?: ?(event: PickerIOSChangeEvent) => mixed, - onValueChange?: ?(newValue: any, newIndex: number) => mixed, - selectedValue: any, + onValueChange?: ?(itemValue: string | number, itemIndex: number) => mixed, + selectedValue: ?(number | string), |}>; type State = {| @@ -73,7 +70,7 @@ type State = {| type ItemProps = $ReadOnly<{| label: ?Label, - value?: ?any, + value?: ?(number | string), color?: ?ColorValue, |}>; @@ -110,10 +107,11 @@ class PickerIOS extends React.Component { render() { return ( - { this._picker = picker; }} + testID={this.props.testID} style={[styles.pickerIOS, this.props.itemStyle]} items={this.state.items} selectedIndex={this.state.selectedIndex} diff --git a/Libraries/Components/Picker/RCTPickerNativeComponent.js b/Libraries/Components/Picker/RCTPickerNativeComponent.js new file mode 100644 index 00000000000000..2a3e8b5e0d6af0 --- /dev/null +++ b/Libraries/Components/Picker/RCTPickerNativeComponent.js @@ -0,0 +1,47 @@ +/** + * 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 + * @format + */ +'use strict'; + +const requireNativeComponent = require('requireNativeComponent'); + +import type {SyntheticEvent} from 'CoreEventTypes'; +import type {TextStyleProp} from 'StyleSheet'; +import type {NativeComponent} from 'ReactNative'; + +type PickerIOSChangeEvent = SyntheticEvent< + $ReadOnly<{| + newValue: number | string, + newIndex: number, + |}>, +>; + +type RCTPickerIOSItemType = $ReadOnly<{| + label: ?Label, + value: ?(number | string), + textColor: ?number, +|}>; + +type Label = Stringish | number; + +type RCTPickerIOSType = Class< + NativeComponent< + $ReadOnly<{| + items: $ReadOnlyArray, + onChange: (event: PickerIOSChangeEvent) => void, + onResponderTerminationRequest: () => boolean, + onStartShouldSetResponder: () => boolean, + selectedIndex: number, + style?: ?TextStyleProp, + testID?: ?string, + |}>, + >, +>; + +module.exports = ((requireNativeComponent('RCTPicker'): any): RCTPickerIOSType); diff --git a/Libraries/Components/ProgressBarAndroid/ProgressBarAndroid.android.js b/Libraries/Components/ProgressBarAndroid/ProgressBarAndroid.android.js index 856f7116b59e0a..616dfc7ed7e07c 100644 --- a/Libraries/Components/ProgressBarAndroid/ProgressBarAndroid.android.js +++ b/Libraries/Components/ProgressBarAndroid/ProgressBarAndroid.android.js @@ -1,10 +1,10 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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 + * @flow * @format */ @@ -12,14 +12,11 @@ const React = require('React'); -const requireNativeComponent = require('requireNativeComponent'); +const ProgressBarAndroidNativeComponent = require('ProgressBarAndroidNativeComponent'); -import type {NativeComponent} from 'ReactNative'; import type {ViewProps} from 'ViewPropTypes'; -const AndroidProgressBar = requireNativeComponent('AndroidProgressBar'); - -type Props = $ReadOnly<{| +export type ProgressBarAndroidProps = $ReadOnly<{| ...ViewProps, /** @@ -83,19 +80,24 @@ type Props = $ReadOnly<{| * ``` */ const ProgressBarAndroid = ( - props: Props, - forwardedRef: ?React.Ref<'AndroidProgressBar'>, + props: ProgressBarAndroidProps, + forwardedRef: ?React.Ref, ) => { - return ; + return ; }; -// $FlowFixMe - TODO T29156721 `React.forwardRef` is not defined in Flow, yet. 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, }; -module.exports = (ProgressBarAndroidToExport: Class>); +/* $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. */ +module.exports = (ProgressBarAndroidToExport: ProgressBarAndroidNativeComponent); diff --git a/Libraries/Components/ProgressBarAndroid/ProgressBarAndroid.ios.js b/Libraries/Components/ProgressBarAndroid/ProgressBarAndroid.ios.js index 260d559929796f..7a00b6636cf664 100644 --- a/Libraries/Components/ProgressBarAndroid/ProgressBarAndroid.ios.js +++ b/Libraries/Components/ProgressBarAndroid/ProgressBarAndroid.ios.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/Components/ProgressBarAndroid/ProgressBarAndroidNativeComponent.js b/Libraries/Components/ProgressBarAndroid/ProgressBarAndroidNativeComponent.js new file mode 100644 index 00000000000000..8e897540f7543d --- /dev/null +++ b/Libraries/Components/ProgressBarAndroid/ProgressBarAndroidNativeComponent.js @@ -0,0 +1,33 @@ +/** + * 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 + * @format + */ + +'use strict'; + +const requireNativeComponent = require('requireNativeComponent'); + +import type {ViewProps} from 'ViewPropTypes'; +import type {NativeComponent} from 'ReactNative'; + +type NativeProps = $ReadOnly<{| + ...ViewProps, + styleAttr?: string, + typeAttr?: string, + indeterminate: boolean, + progress?: number, + animating?: ?boolean, + color?: ?string, + testID?: ?string, +|}>; + +type ProgressBarAndroidType = Class>; + +module.exports = ((requireNativeComponent( + 'AndroidProgressBar', +): any): ProgressBarAndroidType); diff --git a/Libraries/Components/ProgressViewIOS/ProgressViewIOS.android.js b/Libraries/Components/ProgressViewIOS/ProgressViewIOS.android.js index 8caa73db33e494..1b5a1436e4f1ba 100644 --- a/Libraries/Components/ProgressViewIOS/ProgressViewIOS.android.js +++ b/Libraries/Components/ProgressViewIOS/ProgressViewIOS.android.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/Components/ProgressViewIOS/ProgressViewIOS.ios.js b/Libraries/Components/ProgressViewIOS/ProgressViewIOS.ios.js index ea92fccb980583..c54e48a82bac0a 100644 --- a/Libraries/Components/ProgressViewIOS/ProgressViewIOS.ios.js +++ b/Libraries/Components/ProgressViewIOS/ProgressViewIOS.ios.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -10,82 +10,62 @@ 'use strict'; -const Image = require('Image'); -const NativeMethodsMixin = require('NativeMethodsMixin'); const React = require('React'); -const ReactNative = require('ReactNative'); -const PropTypes = require('prop-types'); const StyleSheet = require('StyleSheet'); -const ViewPropTypes = require('ViewPropTypes'); -const createReactClass = require('create-react-class'); -const requireNativeComponent = require('requireNativeComponent'); +const RCTProgressViewNativeComponent = require('RCTProgressViewNativeComponent'); import type {ImageSource} from 'ImageSource'; import type {ColorValue} from 'StyleSheetTypes'; import type {ViewProps} from 'ViewPropTypes'; -const RCTProgressView = requireNativeComponent('RCTProgressView'); - type Props = $ReadOnly<{| ...ViewProps, + + /** + * The progress bar style. + */ progressViewStyle?: ?('default' | 'bar'), + + /** + * The progress value (between 0 and 1). + */ progress?: ?number, + + /** + * The tint color of the progress bar itself. + */ progressTintColor?: ?ColorValue, - trackTintColor?: ?string, + + /** + * The tint color of the progress bar track. + */ + trackTintColor?: ?ColorValue, + + /** + * A stretchable image to display as the progress bar. + */ progressImage?: ?ImageSource, + + /** + * A stretchable image to display behind the progress bar. + */ trackImage?: ?ImageSource, |}>; /** * Use `ProgressViewIOS` to render a UIProgressView on iOS. */ -const ProgressViewIOS = createReactClass({ - displayName: 'ProgressViewIOS', - mixins: [NativeMethodsMixin], - - propTypes: { - ...ViewPropTypes, - /** - * The progress bar style. - */ - progressViewStyle: PropTypes.oneOf(['default', 'bar']), - - /** - * The progress value (between 0 and 1). - */ - progress: PropTypes.number, - - /** - * The tint color of the progress bar itself. - */ - progressTintColor: PropTypes.string, - - /** - * The tint color of the progress bar track. - */ - trackTintColor: PropTypes.string, - - /** - * A stretchable image to display as the progress bar. - */ - progressImage: Image.propTypes.source, - - /** - * A stretchable image to display behind the progress bar. - */ - trackImage: Image.propTypes.source, - }, - - render: function() { - return ( - - ); - }, -}); +const ProgressViewIOS = ( + props: Props, + forwardedRef?: ?React.Ref, +) => ( + +); const styles = StyleSheet.create({ progressView: { @@ -93,6 +73,9 @@ const styles = StyleSheet.create({ }, }); -module.exports = ((ProgressViewIOS: any): Class< - ReactNative.NativeComponent, ->); +const ProgressViewIOSWithRef = React.forwardRef(ProgressViewIOS); + +/* $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. */ +module.exports = (ProgressViewIOSWithRef: RCTProgressViewNativeComponent); diff --git a/Libraries/Components/ProgressViewIOS/RCTProgressViewNativeComponent.js b/Libraries/Components/ProgressViewIOS/RCTProgressViewNativeComponent.js new file mode 100644 index 00000000000000..32b2975a59940a --- /dev/null +++ b/Libraries/Components/ProgressViewIOS/RCTProgressViewNativeComponent.js @@ -0,0 +1,34 @@ +/** + * 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 + * @format + */ + +'use strict'; + +const requireNativeComponent = require('requireNativeComponent'); + +import type {NativeComponent} from 'ReactNative'; +import type {ImageSource} from 'ImageSource'; +import type {ColorValue} from 'StyleSheetTypes'; +import type {ViewProps} from 'ViewPropTypes'; + +type NativeProps = $ReadOnly<{| + ...ViewProps, + progressViewStyle?: ?('default' | 'bar'), + progress?: ?number, + progressTintColor?: ?ColorValue, + trackTintColor?: ?ColorValue, + progressImage?: ?ImageSource, + trackImage?: ?ImageSource, +|}>; + +type NativeProgressViewIOS = Class>; + +module.exports = ((requireNativeComponent( + 'RCTProgressView', +): any): NativeProgressViewIOS); diff --git a/Libraries/Components/RefreshControl/AndroidSwipeRefreshLayoutNativeComponent.js b/Libraries/Components/RefreshControl/AndroidSwipeRefreshLayoutNativeComponent.js new file mode 100644 index 00000000000000..78558aac78b808 --- /dev/null +++ b/Libraries/Components/RefreshControl/AndroidSwipeRefreshLayoutNativeComponent.js @@ -0,0 +1,68 @@ +/** + * 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 requireNativeComponent = require('requireNativeComponent'); + +import type {ColorValue} from 'StyleSheetTypes'; +import type {ViewProps} from 'ViewPropTypes'; +import type {NativeComponent} from 'ReactNative'; + +const AndroidSwipeRefreshLayout = require('UIManager').getViewManagerConfig( + 'AndroidSwipeRefreshLayout', +); +const RefreshLayoutConsts = AndroidSwipeRefreshLayout + ? AndroidSwipeRefreshLayout.Constants + : {SIZE: {}}; + +type NativeProps = $ReadOnly<{| + ...ViewProps, + + /** + * Whether the pull to refresh functionality is enabled. + */ + enabled?: ?boolean, + /** + * The colors (at least one) that will be used to draw the refresh indicator. + */ + colors?: ?$ReadOnlyArray, + /** + * The background color of the refresh indicator. + */ + progressBackgroundColor?: ?ColorValue, + /** + * Size of the refresh indicator, see RefreshControl.SIZE. + */ + size?: ?( + | typeof RefreshLayoutConsts.SIZE.DEFAULT + | typeof RefreshLayoutConsts.SIZE.LARGE + ), + /** + * Progress view top offset + */ + progressViewOffset?: ?number, + + /** + * Called when the view starts refreshing. + */ + onRefresh?: ?() => mixed, + + /** + * Whether the view should be indicating an active refresh. + */ + refreshing: boolean, +|}>; + +type AndroidSwipeRefreshLayoutNativeType = Class>; + +module.exports = ((requireNativeComponent( + 'AndroidSwipeRefreshLayout', +): any): AndroidSwipeRefreshLayoutNativeType); diff --git a/Libraries/Components/RefreshControl/RCTRefreshControlNativeComponent.js b/Libraries/Components/RefreshControl/RCTRefreshControlNativeComponent.js new file mode 100644 index 00000000000000..a3262d46436e60 --- /dev/null +++ b/Libraries/Components/RefreshControl/RCTRefreshControlNativeComponent.js @@ -0,0 +1,50 @@ +/** + * 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 requireNativeComponent = require('requireNativeComponent'); + +import type {ColorValue} from 'StyleSheetTypes'; +import type {ViewProps} from 'ViewPropTypes'; +import type {NativeComponent} from 'ReactNative'; + +export type NativeProps = $ReadOnly<{| + ...ViewProps, + + /** + * The color of the refresh indicator. + */ + tintColor?: ?ColorValue, + /** + * Title color. + */ + titleColor?: ?ColorValue, + /** + * The title displayed under the refresh indicator. + */ + title?: ?string, + + /** + * Called when the view starts refreshing. + */ + onRefresh?: ?() => mixed, + + /** + * Whether the view should be indicating an active refresh. + */ + refreshing: boolean, +|}>; + +type RCTRefreshControlNativeType = Class>; + +module.exports = ((requireNativeComponent( + 'RCTRefreshControl', +): any): RCTRefreshControlNativeType); diff --git a/Libraries/Components/RefreshControl/RefreshControl.js b/Libraries/Components/RefreshControl/RefreshControl.js index 9b287cc4a180b9..bbb3246a47d50e 100644 --- a/Libraries/Components/RefreshControl/RefreshControl.js +++ b/Libraries/Components/RefreshControl/RefreshControl.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -14,27 +14,24 @@ const Platform = require('Platform'); const React = require('React'); const {NativeComponent} = require('ReactNative'); -const requireNativeComponent = require('requireNativeComponent'); -const nullthrows = require('fbjs/lib/nullthrows'); +const AndroidSwipeRefreshLayoutNativeComponent = require('AndroidSwipeRefreshLayoutNativeComponent'); +const RCTRefreshControlNativeComponent = require('RCTRefreshControlNativeComponent'); +const nullthrows = require('nullthrows'); import type {ColorValue} from 'StyleSheetTypes'; import type {ViewProps} from 'ViewPropTypes'; +let RefreshLayoutConsts; if (Platform.OS === 'android') { - const AndroidSwipeRefreshLayout = require('UIManager') - .AndroidSwipeRefreshLayout; - var RefreshLayoutConsts = AndroidSwipeRefreshLayout + const AndroidSwipeRefreshLayout = require('UIManager').getViewManagerConfig( + 'AndroidSwipeRefreshLayout', + ); + RefreshLayoutConsts = AndroidSwipeRefreshLayout ? AndroidSwipeRefreshLayout.Constants : {SIZE: {}}; } else { - var RefreshLayoutConsts = {SIZE: {}}; + RefreshLayoutConsts = {SIZE: {}}; } -type NativeRefreshControlType = Class>; - -const NativeRefreshControl: NativeRefreshControlType = - Platform.OS === 'ios' - ? (requireNativeComponent('RCTRefreshControl'): any) - : (requireNativeComponent('AndroidSwipeRefreshLayout'): any); type IOSProps = $ReadOnly<{| /** @@ -77,7 +74,7 @@ type AndroidProps = $ReadOnly<{| progressViewOffset?: ?number, |}>; -type Props = $ReadOnly<{| +export type RefreshControlProps = $ReadOnly<{| ...ViewProps, ...IOSProps, ...AndroidProps, @@ -85,7 +82,7 @@ type Props = $ReadOnly<{| /** * Called when the view starts refreshing. */ - onRefresh?: ?Function, + onRefresh?: ?() => mixed, /** * Whether the view should be indicating an active refresh. @@ -138,24 +135,27 @@ type Props = $ReadOnly<{| * __Note:__ `refreshing` is a controlled prop, this is why it needs to be set to true * in the `onRefresh` function otherwise the refresh indicator will stop immediately. */ -class RefreshControl extends React.Component { +class RefreshControl extends React.Component { static SIZE = RefreshLayoutConsts.SIZE; - _nativeRef: ?React.ElementRef = null; + _setNativePropsOnRef: ?({refreshing: boolean}) => void; _lastNativeRefreshing = false; componentDidMount() { this._lastNativeRefreshing = this.props.refreshing; } - componentDidUpdate(prevProps: Props) { + componentDidUpdate(prevProps: RefreshControlProps) { // RefreshControl is a controlled component so if the native refreshing // value doesn't match the current js refreshing prop update it to // the js value. if (this.props.refreshing !== prevProps.refreshing) { this._lastNativeRefreshing = this.props.refreshing; - } else if (this.props.refreshing !== this._lastNativeRefreshing) { - nullthrows(this._nativeRef).setNativeProps({ + } else if ( + this.props.refreshing !== this._lastNativeRefreshing && + this._setNativePropsOnRef + ) { + this._setNativePropsOnRef({ refreshing: this.props.refreshing, }); this._lastNativeRefreshing = this.props.refreshing; @@ -163,15 +163,34 @@ class RefreshControl extends React.Component { } render() { - return ( - { - this._nativeRef = ref; - }} - onRefresh={this._onRefresh} - /> - ); + const setRef = ref => + (this._setNativePropsOnRef = ref ? ref.setNativeProps.bind(ref) : null); + if (Platform.OS === 'ios') { + const { + enabled, + colors, + progressBackgroundColor, + size, + progressViewOffset, + ...props + } = this.props; + return ( + + ); + } else { + const {tintColor, titleColor, title, ...props} = this.props; + return ( + + ); + } } _onRefresh = () => { diff --git a/Libraries/Components/RefreshControl/__mocks__/RefreshControlMock.js b/Libraries/Components/RefreshControl/__mocks__/RefreshControlMock.js index bc03b7afa87878..daa576350aa120 100644 --- a/Libraries/Components/RefreshControl/__mocks__/RefreshControlMock.js +++ b/Libraries/Components/RefreshControl/__mocks__/RefreshControlMock.js @@ -1,11 +1,11 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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 + * @flow strict-local */ 'use strict'; diff --git a/Libraries/Components/SafeAreaView/RCTSafeAreaViewNativeComponent.js b/Libraries/Components/SafeAreaView/RCTSafeAreaViewNativeComponent.js new file mode 100644 index 00000000000000..250c2cd5c255be --- /dev/null +++ b/Libraries/Components/SafeAreaView/RCTSafeAreaViewNativeComponent.js @@ -0,0 +1,25 @@ +/** + * 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 + */ + +const requireNativeComponent = require('requireNativeComponent'); + +import type {ViewProps} from 'ViewPropTypes'; +import type {NativeComponent} from 'ReactNative'; + +type NativeProps = $ReadOnly<{| + ...ViewProps, + emulateUnlessSupported?: boolean, +|}>; + +type RCTSafeAreaViewNativeType = Class>; + +module.exports = ((requireNativeComponent( + 'RCTSafeAreaView', +): any): RCTSafeAreaViewNativeType); diff --git a/Libraries/Components/SafeAreaView/SafeAreaView.android.js b/Libraries/Components/SafeAreaView/SafeAreaView.android.js deleted file mode 100644 index f730bc6ab41345..00000000000000 --- a/Libraries/Components/SafeAreaView/SafeAreaView.android.js +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * - * 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 - */ - -'use strict'; - -module.exports = require('View'); diff --git a/Libraries/Components/SafeAreaView/SafeAreaView.ios.js b/Libraries/Components/SafeAreaView/SafeAreaView.ios.js deleted file mode 100644 index 5fb0406b8d5125..00000000000000 --- a/Libraries/Components/SafeAreaView/SafeAreaView.ios.js +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @flow - * @format - */ - -const React = require('React'); -const ViewPropTypes = require('ViewPropTypes'); -const requireNativeComponent = require('requireNativeComponent'); - -import type {ViewProps} from 'ViewPropTypes'; - -const RCTSafeAreaView = requireNativeComponent('RCTSafeAreaView'); - -type Props = ViewProps & { - children: any, -}; - -/** - * Renders nested content and automatically applies paddings reflect the portion of the view - * that is not covered by navigation bars, tab bars, toolbars, and other ancestor views. - * Moreover, and most importantly, Safe Area's paddings reflect physical limitation of the screen, - * such as rounded corners or camera notches (aka sensor housing area on iPhone X). - */ -class SafeAreaView extends React.Component { - static propTypes = { - ...ViewPropTypes, - }; - - render() { - return ; - } -} - -module.exports = SafeAreaView; diff --git a/Libraries/Components/SafeAreaView/SafeAreaView.js b/Libraries/Components/SafeAreaView/SafeAreaView.js new file mode 100644 index 00000000000000..916852aa42607d --- /dev/null +++ b/Libraries/Components/SafeAreaView/SafeAreaView.js @@ -0,0 +1,54 @@ +/** + * 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 + */ + +const Platform = require('Platform'); +const React = require('React'); +const View = require('View'); + +import type {ViewProps} from 'ViewPropTypes'; + +type Props = $ReadOnly<{| + ...ViewProps, + emulateUnlessSupported?: boolean, +|}>; + +let exported; + +/** + * Renders nested content and automatically applies paddings reflect the portion + * of the view that is not covered by navigation bars, tab bars, toolbars, and + * other ancestor views. + * + * Moreover, and most importantly, Safe Area's paddings reflect physical + * limitation of the screen, such as rounded corners or camera notches (aka + * sensor housing area on iPhone X). + */ +if (Platform.OS === 'android') { + exported = class SafeAreaView extends React.Component { + render(): React.Node { + const {emulateUnlessSupported, ...props} = this.props; + return ; + } + }; +} else { + const RCTSafeAreaViewNativeComponent = require('RCTSafeAreaViewNativeComponent'); + exported = class SafeAreaView extends React.Component { + render(): React.Node { + return ( + + ); + } + }; +} + +module.exports = exported; diff --git a/Libraries/Components/ScrollResponder.js b/Libraries/Components/ScrollResponder.js index 548efba80da48c..6b37dadda3b1bc 100644 --- a/Libraries/Components/ScrollResponder.js +++ b/Libraries/Components/ScrollResponder.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -14,17 +14,20 @@ const Dimensions = require('Dimensions'); const FrameRateLogger = require('FrameRateLogger'); const Keyboard = require('Keyboard'); const ReactNative = require('ReactNative'); -const Subscribable = require('Subscribable'); const TextInputState = require('TextInputState'); const UIManager = require('UIManager'); -const invariant = require('fbjs/lib/invariant'); -const nullthrows = require('fbjs/lib/nullthrows'); +const invariant = require('invariant'); +const nullthrows = require('nullthrows'); const performanceNow = require('fbjs/lib/performanceNow'); const warning = require('fbjs/lib/warning'); const {ScrollViewManager} = require('NativeModules'); +import type {PressEvent, ScrollEvent} from 'CoreEventTypes'; +import type {KeyboardEvent} from 'Keyboard'; +import type EmitterSubscription from 'EmitterSubscription'; + /** * Mixin that can be integrated in order to handle scrolling that plays well * with `ResponderEventPlugin`. Integrate with your platform specific scroll @@ -105,17 +108,19 @@ const {ScrollViewManager} = require('NativeModules'); const IS_ANIMATING_TOUCH_START_THRESHOLD_MS = 16; -type State = { +export type State = {| isTouching: boolean, lastMomentumScrollBeginTime: number, lastMomentumScrollEndTime: number, observedScrollSinceBecomingResponder: boolean, becameResponderWhileAnimating: boolean, -}; -type Event = Object; +|}; const ScrollResponderMixin = { - mixins: [Subscribable.Mixin], + _subscriptionKeyboardWillShow: (null: ?EmitterSubscription), + _subscriptionKeyboardWillHide: (null: ?EmitterSubscription), + _subscriptionKeyboardDidShow: (null: ?EmitterSubscription), + _subscriptionKeyboardDidHide: (null: ?EmitterSubscription), scrollResponderMixinGetInitialState: function(): State { return { isTouching: false, @@ -164,7 +169,9 @@ const ScrollResponderMixin = { * true. * */ - scrollResponderHandleStartShouldSetResponder: function(e: Event): boolean { + scrollResponderHandleStartShouldSetResponder: function( + e: PressEvent, + ): boolean { const currentlyFocusedTextInput = TextInputState.currentlyFocusedField(); if ( @@ -189,7 +196,7 @@ const ScrollResponderMixin = { * Invoke this from an `onStartShouldSetResponderCapture` event. */ scrollResponderHandleStartShouldSetResponderCapture: function( - e: Event, + e: PressEvent, ): boolean { // The scroll view should receive taps instead of its descendants if: // * it is already animating/decelerating @@ -208,6 +215,7 @@ const ScrollResponderMixin = { if ( keyboardNeverPersistTaps && currentlyFocusedTextInput != null && + e.target && !TextInputState.isTextInput(e.target) ) { return true; @@ -250,9 +258,9 @@ const ScrollResponderMixin = { /** * Invoke this from an `onTouchEnd` event. * - * @param {SyntheticEvent} e Event. + * @param {PressEvent} e Event. */ - scrollResponderHandleTouchEnd: function(e: Event) { + scrollResponderHandleTouchEnd: function(e: PressEvent) { const nativeEvent = e.nativeEvent; this.state.isTouching = nativeEvent.touches.length !== 0; this.props.onTouchEnd && this.props.onTouchEnd(e); @@ -261,9 +269,9 @@ const ScrollResponderMixin = { /** * Invoke this from an `onTouchCancel` event. * - * @param {SyntheticEvent} e Event. + * @param {PressEvent} e Event. */ - scrollResponderHandleTouchCancel: function(e: Event) { + scrollResponderHandleTouchCancel: function(e: PressEvent) { this.state.isTouching = false; this.props.onTouchCancel && this.props.onTouchCancel(e); }, @@ -271,7 +279,7 @@ const ScrollResponderMixin = { /** * Invoke this from an `onResponderRelease` event. */ - scrollResponderHandleResponderRelease: function(e: Event) { + scrollResponderHandleResponderRelease: function(e: PressEvent) { this.props.onResponderRelease && this.props.onResponderRelease(e); // By default scroll views will unfocus a textField @@ -291,7 +299,7 @@ const ScrollResponderMixin = { } }, - scrollResponderHandleScroll: function(e: Event) { + scrollResponderHandleScroll: function(e: ScrollEvent) { this.state.observedScrollSinceBecomingResponder = true; this.props.onScroll && this.props.onScroll(e); }, @@ -299,7 +307,7 @@ const ScrollResponderMixin = { /** * Invoke this from an `onResponderGrant` event. */ - scrollResponderHandleResponderGrant: function(e: Event) { + scrollResponderHandleResponderGrant: function(e: ScrollEvent) { this.state.observedScrollSinceBecomingResponder = false; this.props.onResponderGrant && this.props.onResponderGrant(e); this.state.becameResponderWhileAnimating = this.scrollResponderIsAnimating(); @@ -312,7 +320,7 @@ const ScrollResponderMixin = { * * Invoke this from an `onScrollBeginDrag` event. */ - scrollResponderHandleScrollBeginDrag: function(e: Event) { + scrollResponderHandleScrollBeginDrag: function(e: ScrollEvent) { FrameRateLogger.beginScroll(); // TODO: track all scrolls after implementing onScrollEndAnimation this.props.onScrollBeginDrag && this.props.onScrollBeginDrag(e); }, @@ -320,7 +328,7 @@ const ScrollResponderMixin = { /** * Invoke this from an `onScrollEndDrag` event. */ - scrollResponderHandleScrollEndDrag: function(e: 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. @@ -339,7 +347,7 @@ const ScrollResponderMixin = { /** * Invoke this from an `onMomentumScrollBegin` event. */ - scrollResponderHandleMomentumScrollBegin: function(e: Event) { + scrollResponderHandleMomentumScrollBegin: function(e: ScrollEvent) { this.state.lastMomentumScrollBeginTime = performanceNow(); this.props.onMomentumScrollBegin && this.props.onMomentumScrollBegin(e); }, @@ -347,7 +355,7 @@ const ScrollResponderMixin = { /** * Invoke this from an `onMomentumScrollEnd` event. */ - scrollResponderHandleMomentumScrollEnd: function(e: Event) { + scrollResponderHandleMomentumScrollEnd: function(e: ScrollEvent) { FrameRateLogger.endScroll(); this.state.lastMomentumScrollEndTime = performanceNow(); this.props.onMomentumScrollEnd && this.props.onMomentumScrollEnd(e); @@ -362,9 +370,9 @@ const ScrollResponderMixin = { * responder). The `onResponderReject` won't fire in that case - it only * fires when a *current* responder rejects our request. * - * @param {SyntheticEvent} e Touch Start event. + * @param {PressEvent} e Touch Start event. */ - scrollResponderHandleTouchStart: function(e: Event) { + scrollResponderHandleTouchStart: function(e: PressEvent) { this.state.isTouching = true; this.props.onTouchStart && this.props.onTouchStart(e); }, @@ -378,9 +386,9 @@ const ScrollResponderMixin = { * responder). The `onResponderReject` won't fire in that case - it only * fires when a *current* responder rejects our request. * - * @param {SyntheticEvent} e Touch Start event. + * @param {PressEvent} e Touch Start event. */ - scrollResponderHandleTouchMove: function(e: Event) { + scrollResponderHandleTouchMove: function(e: PressEvent) { this.props.onTouchMove && this.props.onTouchMove(e); }, @@ -405,7 +413,7 @@ const ScrollResponderMixin = { * Components can pass what node to use by defining a `getScrollableNode` * function otherwise `this` is used. */ - scrollResponderGetScrollableNode: function(): any { + scrollResponderGetScrollableNode: function(): ?number { return this.getScrollableNode ? this.getScrollableNode() : ReactNative.findNodeHandle(this); @@ -436,7 +444,7 @@ const ScrollResponderMixin = { } UIManager.dispatchViewManagerCommand( nullthrows(this.scrollResponderGetScrollableNode()), - UIManager.RCTScrollView.Commands.scrollTo, + UIManager.getViewManagerConfig('RCTScrollView').Commands.scrollTo, [x || 0, y || 0, animated !== false], ); }, @@ -454,7 +462,7 @@ const ScrollResponderMixin = { const animated = (options && options.animated) !== false; UIManager.dispatchViewManagerCommand( this.scrollResponderGetScrollableNode(), - UIManager.RCTScrollView.Commands.scrollToEnd, + UIManager.getViewManagerConfig('RCTScrollView').Commands.scrollToEnd, [animated], ); }, @@ -513,7 +521,8 @@ const ScrollResponderMixin = { scrollResponderFlashScrollIndicators: function() { UIManager.dispatchViewManagerCommand( this.scrollResponderGetScrollableNode(), - UIManager.RCTScrollView.Commands.flashScrollIndicators, + UIManager.getViewManagerConfig('RCTScrollView').Commands + .flashScrollIndicators, [], ); }, @@ -522,14 +531,14 @@ const ScrollResponderMixin = { * This method should be used as the callback to onFocus in a TextInputs' * parent view. Note that any module using this mixin needs to return * the parent view's ref in getScrollViewRef() in order to use this method. - * @param {any} nodeHandle The TextInput node handle + * @param {number} 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: any, + nodeHandle: number, additionalOffset?: number, preventNegativeScrollOffset?: boolean, ) { @@ -579,8 +588,8 @@ const ScrollResponderMixin = { this.preventNegativeScrollOffset = false; }, - scrollResponderTextInputFocusError: function(e: Event) { - console.error('Error measuring text field: ', e); + scrollResponderTextInputFocusError: function(msg: string) { + console.error('Error measuring text field: ', msg); }, /** @@ -601,28 +610,40 @@ const ScrollResponderMixin = { this.keyboardWillOpenTo = null; this.additionalScrollOffset = 0; - this.addListenerOn( - Keyboard, + this._subscriptionKeyboardWillShow = Keyboard.addListener( 'keyboardWillShow', this.scrollResponderKeyboardWillShow, ); - this.addListenerOn( - Keyboard, + + this._subscriptionKeyboardWillHide = Keyboard.addListener( 'keyboardWillHide', this.scrollResponderKeyboardWillHide, ); - this.addListenerOn( - Keyboard, + this._subscriptionKeyboardDidShow = Keyboard.addListener( 'keyboardDidShow', this.scrollResponderKeyboardDidShow, ); - this.addListenerOn( - Keyboard, + 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 @@ -651,17 +672,17 @@ const ScrollResponderMixin = { * relevant to you. (For example, only if you receive these callbacks after * you had explicitly focused a node etc). */ - scrollResponderKeyboardWillShow: function(e: Event) { + scrollResponderKeyboardWillShow: function(e: KeyboardEvent) { this.keyboardWillOpenTo = e; this.props.onKeyboardWillShow && this.props.onKeyboardWillShow(e); }, - scrollResponderKeyboardWillHide: function(e: Event) { + scrollResponderKeyboardWillHide: function(e: KeyboardEvent) { this.keyboardWillOpenTo = null; this.props.onKeyboardWillHide && this.props.onKeyboardWillHide(e); }, - scrollResponderKeyboardDidShow: function(e: Event) { + 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) { @@ -670,7 +691,7 @@ const ScrollResponderMixin = { this.props.onKeyboardDidShow && this.props.onKeyboardDidShow(e); }, - scrollResponderKeyboardDidHide: function(e: Event) { + scrollResponderKeyboardDidHide: function(e: KeyboardEvent) { this.keyboardWillOpenTo = null; this.props.onKeyboardDidHide && this.props.onKeyboardDidHide(e); }, diff --git a/Libraries/Components/ScrollView/InternalScrollViewType.js b/Libraries/Components/ScrollView/InternalScrollViewType.js index 471e25d9a5dab9..a13db7dee6a65b 100644 --- a/Libraries/Components/ScrollView/InternalScrollViewType.js +++ b/Libraries/Components/ScrollView/InternalScrollViewType.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/Components/ScrollView/ScrollView.js b/Libraries/Components/ScrollView/ScrollView.js index 9f0688e87489f3..c237e0a6a4a96d 100644 --- a/Libraries/Components/ScrollView/ScrollView.js +++ b/Libraries/Components/ScrollView/ScrollView.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -20,13 +20,11 @@ const StyleSheet = require('StyleSheet'); const View = require('View'); const InternalScrollViewType = require('InternalScrollViewType'); -const createReactClass = require('create-react-class'); const dismissKeyboard = require('dismissKeyboard'); const flattenStyle = require('flattenStyle'); -const invariant = require('fbjs/lib/invariant'); +const invariant = require('invariant'); const processDecelerationRate = require('processDecelerationRate'); const requireNativeComponent = require('requireNativeComponent'); -const warning = require('fbjs/lib/warning'); const resolveAssetSource = require('resolveAssetSource'); import type {PressEvent} from 'CoreEventTypes'; @@ -37,6 +35,7 @@ import type {ViewProps} from 'ViewPropTypes'; import type {PointProp} from 'PointPropType'; import type {ColorValue} from 'StyleSheetTypes'; +import type {State as ScrollResponderState} from 'ScrollResponder'; let AndroidScrollView; let AndroidHorizontalScrollContentView; @@ -125,19 +124,6 @@ type IOSProps = $ReadOnly<{| * @platform ios */ centerContent?: ?boolean, - /** - * A floating-point number that determines how quickly the scroll view - * decelerates after the user lifts their finger. You may also use string - * shortcuts `"normal"` and `"fast"` which match the underlying iOS settings - * for `UIScrollViewDecelerationRateNormal` and - * `UIScrollViewDecelerationRateFast` respectively. - * - * - `'normal'`: 0.998 (the default) - * - `'fast'`: 0.99 - * - * @platform ios - */ - decelerationRate?: ?('fast' | 'normal' | number), /** * The style of the scroll indicators. * @@ -230,6 +216,11 @@ type IOSProps = $ReadOnly<{| * @platform ios */ scrollsToTop?: ?boolean, + /** + * Fires when the scroll view scrolls to top after the status bar has been tapped + * @platform ios + */ + onScrollToTop?: ?Function, /** * When true, shows a horizontal scroll indicator. * The default value is true. @@ -310,6 +301,13 @@ type AndroidProps = $ReadOnly<{| * @platform android */ overScrollMode?: ?('auto' | 'always' | 'never'), + /** + * Causes the scrollbars not to turn transparent when they are not in use. + * The default value is false. + * + * @platform android + */ + persistentScrollbar?: ?boolean, |}>; type VRProps = $ReadOnly<{| @@ -353,6 +351,17 @@ export type Props = $ReadOnly<{| * ``` */ contentContainerStyle?: ?ViewStyleProp, + /** + * A floating-point number that determines how quickly the scroll view + * decelerates after the user lifts their finger. You may also use string + * shortcuts `"normal"` and `"fast"` which match the underlying iOS settings + * for `UIScrollViewDecelerationRateNormal` and + * `UIScrollViewDecelerationRateFast` respectively. + * + * - `'normal'`: 0.998 on iOS, 0.985 on Android (the default) + * - `'fast'`: 0.99 on iOS, 0.9 on Android + */ + decelerationRate?: ?('fast' | 'normal' | number), /** * When true, the scroll view's children are arranged horizontally in a row * instead of vertically in a column. The default value is false. @@ -394,7 +403,9 @@ export type Props = $ReadOnly<{| * - `false`, deprecated, use 'never' instead * - `true`, deprecated, use 'always' instead */ - // $FlowFixMe(site=react_native_fb) Issues found when typing ScrollView + /* $FlowFixMe(>=0.92.0 site=react_native_fb) This comment suppresses an error + * found when Flow v0.92 was deployed. To see the error, delete this comment + * and run Flow. */ keyboardShouldPersistTaps?: ?('always' | 'never' | 'handled' | false | true), /** * Called when the momentum scroll starts (scroll which occurs as the ScrollView glides to a stop). @@ -438,6 +449,7 @@ export type Props = $ReadOnly<{| * Note: Vertical pagination is not supported on Android. */ pagingEnabled?: ?boolean, + /** * When false, the view cannot be scrolled via touch interaction. * The default value is true. @@ -462,12 +474,36 @@ export type Props = $ReadOnly<{| * When set, causes the scroll view to stop at multiples of the value of * `snapToInterval`. This can be used for paginating through children * that have lengths smaller than the scroll view. Typically used in - * combination with `snapToAlignment` and `decelerationRate="fast"` on ios. - * Overrides less configurable `pagingEnabled` prop. + * combination with `snapToAlignment` and `decelerationRate="fast"`. * - * Supported for horizontal scrollview on android. + * Overrides less configurable `pagingEnabled` prop. */ snapToInterval?: ?number, + /** + * When set, causes the scroll view to stop at the defined offsets. + * This can be used for paginating through variously sized children + * that have lengths smaller than the scroll view. Typically used in + * combination with `decelerationRate="fast"`. + * + * Overrides less configurable `pagingEnabled` and `snapToInterval` props. + */ + snapToOffsets?: ?$ReadOnlyArray, + /** + * Use in conjuction with `snapToOffsets`. By default, the beginning + * of the list counts as a snap offset. Set `snapToStart` to false to disable + * this behavior and allow the list to scroll freely between its start and + * the first `snapToOffsets` offset. + * The default value is true. + */ + snapToStart?: ?boolean, + /** + * Use in conjuction with `snapToOffsets`. By default, the end + * of the list counts as a snap offset. Set `snapToEnd` to false to disable + * this behavior and allow the list to scroll freely between its end and + * the last `snapToOffsets` offset. + * The default value is true. + */ + snapToEnd?: ?boolean, /** * Experimental: When true, offscreen child views (whose `overflow` value is * `hidden`) are removed from their native backing superview when offscreen. @@ -483,10 +519,28 @@ export type Props = $ReadOnly<{| * See [RefreshControl](docs/refreshcontrol.html). */ refreshControl?: ?React.Element, - style?: ?ViewStyleProp, children?: React.Node, |}>; +type State = {| + 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; +} + /** * Component that wraps platform ScrollView while providing * integration with touch locking "responder" system. @@ -522,24 +576,71 @@ export type Props = $ReadOnly<{| * multiple columns, infinite scroll loading, or any number of other features it * supports out of the box. */ -const ScrollView = createReactClass({ - displayName: 'ScrollView', - mixins: [ScrollResponder.Mixin], +class ScrollView extends React.Component { + /** + * 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(...args) { + super(...args); + + /** + * 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') + ) { + (this: any)[key] = ScrollResponder.Mixin[key].bind(this); + } + } - _scrollAnimatedValue: (new AnimatedImplementation.Value( + /** + * 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 => { + (this: any)[key] = ScrollResponder.Mixin[key]; + }); + } + + _scrollAnimatedValue: AnimatedImplementation.Value = new AnimatedImplementation.Value( 0, - ): AnimatedImplementation.Value), - _scrollAnimatedValueAttachment: (null: ?{detach: () => void}), - _stickyHeaderRefs: (new Map(): Map), - _headerLayoutYs: (new Map(): Map), - getInitialState: function() { - return { - ...this.scrollResponderMixinGetInitialState(), - layoutHeight: null, - }; - }, + ); + _scrollAnimatedValueAttachment: ?{detach: () => void} = null; + _stickyHeaderRefs: Map = new Map(); + _headerLayoutYs: Map = new Map(); + + state = { + layoutHeight: null, + ...ScrollResponder.Mixin.scrollResponderMixinGetInitialState(), + }; - UNSAFE_componentWillMount: function() { + UNSAFE_componentWillMount() { + this._scrollResponder.UNSAFE_componentWillMount(); this._scrollAnimatedValue = new AnimatedImplementation.Value( this.props.contentOffset ? this.props.contentOffset.y : 0, ); @@ -548,25 +649,26 @@ const ScrollView = createReactClass({ ); this._stickyHeaderRefs = new Map(); this._headerLayoutYs = new Map(); - }, + } - componentDidMount: function() { + componentDidMount() { this._updateAnimatedNodeAttachment(); - }, + } - componentDidUpdate: function() { + componentDidUpdate() { this._updateAnimatedNodeAttachment(); - }, + } - componentWillUnmount: function() { + componentWillUnmount() { + this._scrollResponder.componentWillUnmount(); if (this._scrollAnimatedValueAttachment) { this._scrollAnimatedValueAttachment.detach(); } - }, + } - setNativeProps: function(props: Object) { + setNativeProps(props: Object) { this._scrollViewRef && this._scrollViewRef.setNativeProps(props); - }, + } /** * Returns a reference to the underlying scroll responder, which supports @@ -574,17 +676,23 @@ const ScrollView = createReactClass({ * implement this method so that they can be composed while providing access * to the underlying scroll responder's methods. */ - getScrollResponder: function(): ScrollView { - return this; - }, + getScrollResponder(): { + ...typeof ScrollView, + ...typeof ScrollResponder.Mixin, + } { + return ((this: any): { + ...typeof ScrollView, + ...typeof ScrollResponder.Mixin, + }); + } - getScrollableNode: function(): any { + getScrollableNode(): any { return ReactNative.findNodeHandle(this._scrollViewRef); - }, + } - getInnerViewNode: function(): any { + getInnerViewNode(): any { return ReactNative.findNodeHandle(this._innerViewRef); - }, + } /** * Scrolls to a given x, y offset, either immediately or with a smooth animation. @@ -597,7 +705,7 @@ const ScrollView = createReactClass({ * the function also accepts separate arguments as an alternative to the options object. * This is deprecated due to ambiguity (y before x), and SHOULD NOT BE USED. */ - scrollTo: function( + scrollTo( y?: number | {x?: number, y?: number, animated?: boolean}, x?: number, animated?: boolean, @@ -610,12 +718,12 @@ const ScrollView = createReactClass({ } else { ({x, y, animated} = y || {}); } - this.getScrollResponder().scrollResponderScrollTo({ + this._scrollResponder.scrollResponderScrollTo({ x: x || 0, y: y || 0, animated: animated !== false, }); - }, + } /** * If this is a vertical ScrollView scrolls to the bottom. @@ -625,40 +733,39 @@ const ScrollView = createReactClass({ * `scrollToEnd({animated: false})` for immediate scrolling. * If no options are passed, `animated` defaults to true. */ - scrollToEnd: function(options?: {animated?: boolean}) { + scrollToEnd(options?: {animated?: boolean}) { // Default to true const animated = (options && options.animated) !== false; - this.getScrollResponder().scrollResponderScrollToEnd({ + this._scrollResponder.scrollResponderScrollToEnd({ animated: animated, }); - }, + } /** * Deprecated, use `scrollTo` instead. */ - scrollWithoutAnimationTo: function(y: number = 0, x: number = 0) { + scrollWithoutAnimationTo(y: number = 0, x: number = 0) { console.warn( '`scrollWithoutAnimationTo` is deprecated. Use `scrollTo` instead', ); this.scrollTo({x, y, animated: false}); - }, + } /** * Displays the scroll indicators momentarily. * * @platform ios */ - flashScrollIndicators: function() { - this.getScrollResponder().scrollResponderFlashScrollIndicators(); - }, + flashScrollIndicators() { + this._scrollResponder.scrollResponderFlashScrollIndicators(); + } - _getKeyForIndex: function(index, childArray) { - // $FlowFixMe Invalid prop usage + _getKeyForIndex(index, childArray) { const child = childArray[index]; return child && child.key; - }, + } - _updateAnimatedNodeAttachment: function() { + _updateAnimatedNodeAttachment() { if (this._scrollAnimatedValueAttachment) { this._scrollAnimatedValueAttachment.detach(); } @@ -672,18 +779,19 @@ const ScrollView = createReactClass({ [{nativeEvent: {contentOffset: {y: this._scrollAnimatedValue}}}], ); } - }, + } - _setStickyHeaderRef: function(key, ref) { + _setStickyHeaderRef(key, ref) { if (ref) { this._stickyHeaderRefs.set(key, ref); } else { this._stickyHeaderRefs.delete(key); } - }, + } - _onStickyHeaderLayout: function(index, event, key) { - if (!this.props.stickyHeaderIndices) { + _onStickyHeaderLayout(index, event, key) { + const {stickyHeaderIndices} = this.props; + if (!stickyHeaderIndices) { return; } const childArray = React.Children.toArray(this.props.children); @@ -695,19 +803,17 @@ const ScrollView = createReactClass({ const layoutY = event.nativeEvent.layout.y; this._headerLayoutYs.set(key, layoutY); - const indexOfIndex = this.props.stickyHeaderIndices.indexOf(index); - const previousHeaderIndex = this.props.stickyHeaderIndices[ - indexOfIndex - 1 - ]; + const indexOfIndex = stickyHeaderIndices.indexOf(index); + const previousHeaderIndex = stickyHeaderIndices[indexOfIndex - 1]; if (previousHeaderIndex != null) { const previousHeader = this._stickyHeaderRefs.get( this._getKeyForIndex(previousHeaderIndex, childArray), ); previousHeader && previousHeader.setNextHeaderY(layoutY); } - }, + } - _handleScroll: function(e: Object) { + _handleScroll = (e: Object) => { if (__DEV__) { if ( this.props.onScroll && @@ -731,35 +837,35 @@ const ScrollView = createReactClass({ dismissKeyboard(); } } - this.scrollResponderHandleScroll(e); - }, + this._scrollResponder.scrollResponderHandleScroll(e); + }; - _handleLayout: function(e: Object) { + _handleLayout = (e: Object) => { if (this.props.invertStickyHeaders) { this.setState({layoutHeight: e.nativeEvent.layout.height}); } if (this.props.onLayout) { this.props.onLayout(e); } - }, + }; - _handleContentOnLayout: function(e: Object) { + _handleContentOnLayout = (e: Object) => { const {width, height} = e.nativeEvent.layout; this.props.onContentSizeChange && this.props.onContentSizeChange(width, height); - }, + }; - _scrollViewRef: (null: ?ScrollView), - _setScrollViewRef: function(ref: ?ScrollView) { + _scrollViewRef: ?ScrollView = null; + _setScrollViewRef = (ref: ?ScrollView) => { this._scrollViewRef = ref; - }, + }; - _innerViewRef: (null: ?NativeMethodsMixinType), - _setInnerViewRef: function(ref: ?NativeMethodsMixinType) { + _innerViewRef: ?NativeMethodsMixinType = null; + _setInnerViewRef = (ref: ?NativeMethodsMixinType) => { this._innerViewRef = ref; - }, + }; - render: function() { + render() { let ScrollViewClass; let ScrollContentContainerViewClass; if (Platform.OS === 'android') { @@ -773,10 +879,6 @@ const ScrollView = createReactClass({ } else { ScrollViewClass = RCTScrollView; ScrollContentContainerViewClass = RCTScrollContentView; - warning( - this.props.snapToInterval == null || !this.props.pagingEnabled, - 'snapToInterval is currently ignored when pagingEnabled is true.', - ); } invariant( @@ -814,38 +916,39 @@ const ScrollView = createReactClass({ } const {stickyHeaderIndices} = this.props; + let children = this.props.children; + + if (stickyHeaderIndices != null && stickyHeaderIndices.length > 0) { + const childArray = React.Children.toArray(this.props.children); + + children = childArray.map((child, index) => { + const indexOfIndex = child ? stickyHeaderIndices.indexOf(index) : -1; + if (indexOfIndex > -1) { + const key = child.key; + const nextIndex = stickyHeaderIndices[indexOfIndex + 1]; + return ( + this._setStickyHeaderRef(key, ref)} + nextHeaderLayoutY={this._headerLayoutYs.get( + this._getKeyForIndex(nextIndex, childArray), + )} + onLayout={event => this._onStickyHeaderLayout(index, event, key)} + scrollAnimatedValue={this._scrollAnimatedValue} + inverted={this.props.invertStickyHeaders} + scrollViewHeight={this.state.layoutHeight}> + {child} + + ); + } else { + return child; + } + }); + } + const hasStickyHeaders = stickyHeaderIndices && stickyHeaderIndices.length > 0; - const childArray = - hasStickyHeaders && React.Children.toArray(this.props.children); - const children = hasStickyHeaders - ? // $FlowFixMe Invalid prop usage - childArray.map((child, index) => { - const indexOfIndex = child ? stickyHeaderIndices.indexOf(index) : -1; - if (indexOfIndex > -1) { - const key = child.key; - const nextIndex = stickyHeaderIndices[indexOfIndex + 1]; - return ( - this._setStickyHeaderRef(key, ref)} - nextHeaderLayoutY={this._headerLayoutYs.get( - this._getKeyForIndex(nextIndex, childArray), - )} - onLayout={event => - this._onStickyHeaderLayout(index, event, key) - } - scrollAnimatedValue={this._scrollAnimatedValue} - inverted={this.props.invertStickyHeaders} - scrollViewHeight={this.state.layoutHeight}> - {child} - - ); - } else { - return child; - } - }) - : this.props.children; + const contentContainer = ( {Platform.isTV ? null : refreshControl} {contentContainer} @@ -952,6 +1080,7 @@ const ScrollView = createReactClass({ {contentContainer} , @@ -959,12 +1088,13 @@ const ScrollView = createReactClass({ } } return ( + // $FlowFixMe {contentContainer} ); - }, -}); + } +} const TypedScrollView = ((ScrollView: any): Class< InternalScrollViewType, diff --git a/Libraries/Components/ScrollView/ScrollViewStickyHeader.js b/Libraries/Components/ScrollView/ScrollViewStickyHeader.js index e17467b264cd98..5ad409c783e96f 100644 --- a/Libraries/Components/ScrollView/ScrollViewStickyHeader.js +++ b/Libraries/Components/ScrollView/ScrollViewStickyHeader.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/Components/ScrollView/__mocks__/ScrollViewMock.js b/Libraries/Components/ScrollView/__mocks__/ScrollViewMock.js index 46f691382d2f6e..a2367c357d3189 100644 --- a/Libraries/Components/ScrollView/__mocks__/ScrollViewMock.js +++ b/Libraries/Components/ScrollView/__mocks__/ScrollViewMock.js @@ -1,19 +1,17 @@ /** - * Copyright (c) 2013-present, Facebook, Inc. + * 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 + * @flow strict-local */ /* eslint-env jest */ 'use strict'; -declare var jest: any; - const React = require('React'); const View = require('View'); diff --git a/Libraries/Components/ScrollView/processDecelerationRate.js b/Libraries/Components/ScrollView/processDecelerationRate.js index 18b903ffa3831c..a685381715d835 100644 --- a/Libraries/Components/ScrollView/processDecelerationRate.js +++ b/Libraries/Components/ScrollView/processDecelerationRate.js @@ -1,19 +1,30 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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'; -function processDecelerationRate(decelerationRate) { +const Platform = require('Platform'); + +function processDecelerationRate( + decelerationRate: number | 'normal' | 'fast', +): number { if (decelerationRate === 'normal') { - decelerationRate = 0.998; + return Platform.select({ + ios: 0.998, + android: 0.985, + }); } else if (decelerationRate === 'fast') { - decelerationRate = 0.99; + return Platform.select({ + ios: 0.99, + android: 0.9, + }); } return decelerationRate; } diff --git a/Libraries/Components/SegmentedControlIOS/RCTSegmentedControlNativeComponent.js b/Libraries/Components/SegmentedControlIOS/RCTSegmentedControlNativeComponent.js new file mode 100644 index 00000000000000..37b944d186fc0a --- /dev/null +++ b/Libraries/Components/SegmentedControlIOS/RCTSegmentedControlNativeComponent.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. + * + * @flow + * @format + */ +'use strict'; + +const requireNativeComponent = require('requireNativeComponent'); + +import type {ViewProps} from 'ViewPropTypes'; +import type {SyntheticEvent} from 'CoreEventTypes'; +import type {NativeComponent} from 'ReactNative'; + +type Event = SyntheticEvent< + $ReadOnly<{| + value: number, + selectedSegmentIndex: number, + |}>, +>; + +type SegmentedControlIOSProps = $ReadOnly<{| + ...ViewProps, + /** + * The labels for the control's segment buttons, in order. + */ + values?: $ReadOnlyArray, + /** + * The index in `props.values` of the segment to be (pre)selected. + */ + selectedIndex?: ?number, + /** + * Callback that is called when the user taps a segment; + * passes the segment's value as an argument + */ + onValueChange?: ?(value: number) => mixed, + /** + * Callback that is called when the user taps a segment; + * passes the event as an argument + */ + onChange?: ?(event: Event) => mixed, + /** + * If false the user won't be able to interact with the control. + * Default value is true. + */ + enabled?: boolean, + /** + * Accent color of the control. + */ + tintColor?: ?string, + /** + * If true, then selecting a segment won't persist visually. + * The `onValueChange` callback will still work as expected. + */ + momentary?: ?boolean, +|}>; + +type NativeSegmentedControlIOS = Class< + NativeComponent, +>; + +module.exports = ((requireNativeComponent( + 'RCTSegmentedControl', +): any): NativeSegmentedControlIOS); diff --git a/Libraries/Components/SegmentedControlIOS/SegmentedControlIOS.android.js b/Libraries/Components/SegmentedControlIOS/SegmentedControlIOS.android.js index 7a988960ceca96..581e77cf6ff8d4 100644 --- a/Libraries/Components/SegmentedControlIOS/SegmentedControlIOS.android.js +++ b/Libraries/Components/SegmentedControlIOS/SegmentedControlIOS.android.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/Components/SegmentedControlIOS/SegmentedControlIOS.ios.js b/Libraries/Components/SegmentedControlIOS/SegmentedControlIOS.ios.js index b4635f13f8e4d5..8dc88c1c7af478 100644 --- a/Libraries/Components/SegmentedControlIOS/SegmentedControlIOS.ios.js +++ b/Libraries/Components/SegmentedControlIOS/SegmentedControlIOS.ios.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -10,39 +10,61 @@ 'use strict'; -const NativeMethodsMixin = require('NativeMethodsMixin'); const React = require('React'); -const ReactNative = require('ReactNative'); -const PropTypes = require('prop-types'); const StyleSheet = require('StyleSheet'); -const ViewPropTypes = require('ViewPropTypes'); -const createReactClass = require('create-react-class'); -const requireNativeComponent = require('requireNativeComponent'); +const RCTSegmentedControlNativeComponent = require('RCTSegmentedControlNativeComponent'); +import type {SyntheticEvent} from 'CoreEventTypes'; import type {ViewProps} from 'ViewPropTypes'; -const RCTSegmentedControl = requireNativeComponent('RCTSegmentedControl'); +type Event = SyntheticEvent< + $ReadOnly<{| + value: number, + selectedSegmentIndex: number, + |}>, +>; -type DefaultProps = { - values: $ReadOnlyArray, - enabled: boolean, -}; - -type Props = $ReadOnly<{| +type SegmentedControlIOSProps = $ReadOnly<{| ...ViewProps, - values?: ?$ReadOnlyArray, + /** + * The labels for the control's segment buttons, in order. + */ + values?: $ReadOnlyArray, + /** + * The index in `props.values` of the segment to be (pre)selected. + */ selectedIndex?: ?number, - onValueChange?: ?Function, - onChange?: ?Function, - enabled?: ?boolean, + /** + * Callback that is called when the user taps a segment; + * passes the segment's value as an argument + */ + onValueChange?: ?(value: number) => mixed, + /** + * Callback that is called when the user taps a segment; + * passes the event as an argument + */ + onChange?: ?(event: Event) => mixed, + /** + * If false the user won't be able to interact with the control. + * Default value is true. + */ + enabled?: boolean, + /** + * Accent color of the control. + */ tintColor?: ?string, + /** + * If true, then selecting a segment won't persist visually. + * The `onValueChange` callback will still work as expected. + */ momentary?: ?boolean, |}>; -const SEGMENTED_CONTROL_REFERENCE = 'segmentedcontrol'; - -type Event = Object; +type Props = $ReadOnly<{| + ...SegmentedControlIOSProps, + forwardedRef: ?React.Ref, +|}>; /** * Use `SegmentedControlIOS` to render a UISegmentedControl iOS. @@ -64,76 +86,31 @@ type Event = Object; * /> * ```` */ -const SegmentedControlIOS = createReactClass({ - displayName: 'SegmentedControlIOS', - mixins: [NativeMethodsMixin], - - propTypes: { - ...ViewPropTypes, - /** - * The labels for the control's segment buttons, in order. - */ - values: PropTypes.arrayOf(PropTypes.string), - - /** - * The index in `props.values` of the segment to be (pre)selected. - */ - selectedIndex: PropTypes.number, - - /** - * Callback that is called when the user taps a segment; - * passes the segment's value as an argument - */ - onValueChange: PropTypes.func, - - /** - * Callback that is called when the user taps a segment; - * passes the event as an argument - */ - onChange: PropTypes.func, - - /** - * If false the user won't be able to interact with the control. - * Default value is true. - */ - enabled: PropTypes.bool, - /** - * Accent color of the control. - */ - tintColor: PropTypes.string, +class SegmentedControlIOS extends React.Component { + static defaultProps = { + values: [], + enabled: true, + }; - /** - * If true, then selecting a segment won't persist visually. - * The `onValueChange` callback will still work as expected. - */ - momentary: PropTypes.bool, - }, - - getDefaultProps: function(): DefaultProps { - return { - values: [], - enabled: true, - }; - }, - - _onChange: function(event: Event) { + _onChange = (event: Event) => { this.props.onChange && this.props.onChange(event); this.props.onValueChange && this.props.onValueChange(event.nativeEvent.value); - }, + }; - render: function() { + render() { + const {forwardedRef, ...props} = this.props; return ( - ); - }, -}); + } +} const styles = StyleSheet.create({ segmentedControl: { @@ -141,6 +118,16 @@ const styles = StyleSheet.create({ }, }); -module.exports = ((SegmentedControlIOS: any): Class< - ReactNative.NativeComponent, ->); +const SegmentedControlIOSWithRef = React.forwardRef( + ( + props: SegmentedControlIOSProps, + forwardedRef: ?React.Ref, + ) => { + return ; + }, +); + +/* $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. */ +module.exports = (SegmentedControlIOSWithRef: NativeSegmentedControlIOS); diff --git a/Libraries/Components/Slider/RCTSliderNativeComponent.js b/Libraries/Components/Slider/RCTSliderNativeComponent.js new file mode 100644 index 00000000000000..c718c67ee2e2d9 --- /dev/null +++ b/Libraries/Components/Slider/RCTSliderNativeComponent.js @@ -0,0 +1,52 @@ +/** + * 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 requireNativeComponent = require('requireNativeComponent'); + +import type {ColorValue} from 'StyleSheetTypes'; +import type {ImageSource} from 'ImageSource'; +import type {NativeComponent} from 'ReactNative'; +import type {SyntheticEvent} from 'CoreEventTypes'; +import type {ViewProps} from 'ViewPropTypes'; +import type {ViewStyleProp} from 'StyleSheet'; + +type Event = SyntheticEvent< + $ReadOnly<{| + value: number, + fromUser?: boolean, + |}>, +>; + +type NativeProps = $ReadOnly<{| + ...ViewProps, + disabled?: ?boolean, + enabled?: ?boolean, + maximumTrackImage?: ?ImageSource, + maximumTrackTintColor?: ?ColorValue, + maximumValue?: ?number, + minimumTrackImage?: ?ImageSource, + minimumTrackTintColor?: ?ColorValue, + minimumValue?: ?number, + onChange?: ?(event: Event) => void, + onSlidingComplete?: ?(event: Event) => void, + onValueChange?: ?(event: Event) => void, + step?: ?number, + testID?: ?string, + thumbImage?: ?ImageSource, + thumbTintColor?: ?ColorValue, + trackImage?: ?ImageSource, + value?: ?number, +|}>; + +type RCTSliderType = Class>; + +module.exports = ((requireNativeComponent('RCTSlider'): any): RCTSliderType); diff --git a/Libraries/Components/Slider/Slider.js b/Libraries/Components/Slider/Slider.js index 7f18a2d3b35ef0..34391e6a63b5ff 100644 --- a/Libraries/Components/Slider/Slider.js +++ b/Libraries/Components/Slider/Slider.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -10,21 +10,27 @@ 'use strict'; -const ReactNative = require('ReactNative'); const Platform = require('Platform'); +const RCTSliderNativeComponent = require('RCTSliderNativeComponent'); const React = require('React'); +const ReactNative = require('ReactNative'); const StyleSheet = require('StyleSheet'); -const requireNativeComponent = require('requireNativeComponent'); - import type {ImageSource} from 'ImageSource'; import type {ViewStyleProp} from 'StyleSheet'; import type {ColorValue} from 'StyleSheetTypes'; import type {ViewProps} from 'ViewPropTypes'; - -const RCTSlider = requireNativeComponent('RCTSlider'); - -type Event = Object; +import type {SyntheticEvent} from 'CoreEventTypes'; + +type Event = SyntheticEvent< + $ReadOnly<{| + value: number, + /** + * Android Only. + */ + fromUser?: boolean, + |}>, +>; type IOSProps = $ReadOnly<{| /** @@ -51,22 +57,13 @@ type IOSProps = $ReadOnly<{| thumbImage?: ?ImageSource, |}>; -type AndroidProps = $ReadOnly<{| - /** - * Color of the foreground switch grip. - * @platform android - */ - thumbTintColor?: ?ColorValue, -|}>; - type Props = $ReadOnly<{| ...ViewProps, ...IOSProps, - ...AndroidProps, /** * Used to style and layout the `Slider`. See `StyleSheet.js` and - * `ViewStylePropTypes.js` for more info. + * `DeprecatedViewStylePropTypes.js` for more info. */ style?: ?ViewStyleProp, @@ -108,6 +105,11 @@ type Props = $ReadOnly<{| * Overrides the default blue gradient image on iOS. */ maximumTrackTintColor?: ?ColorValue, + /** + * The color used to tint the default thumb images on iOS, or the + * color of the foreground switch grip on Android. + */ + thumbTintColor?: ?ColorValue, /** * If true the user won't be able to move the slider. @@ -118,14 +120,14 @@ type Props = $ReadOnly<{| /** * Callback continuously called while the user is dragging the slider. */ - onValueChange?: ?Function, + onValueChange?: ?(value: number) => void, /** * Callback that is called when the user releases the slider, * regardless if the value has changed. The current value is passed * as an argument to the callback handler. */ - onSlidingComplete?: ?Function, + onSlidingComplete?: ?(value: number) => void, /** * Used to locate this view in UI automation tests. @@ -195,44 +197,43 @@ type Props = $ReadOnly<{| */ const Slider = ( props: Props, - forwardedRef?: ?React.Ref<'RCTActivityIndicatorView'>, + forwardedRef?: ?React.Ref, ) => { const style = StyleSheet.compose( styles.slider, props.style, ); - const onValueChange = - props.onValueChange && - ((event: Event) => { - let userEvent = true; - if (Platform.OS === 'android') { - // On Android there's a special flag telling us the user is - // dragging the slider. - userEvent = event.nativeEvent.fromUser; + const {onValueChange, onSlidingComplete, ...localProps} = props; + + const onValueChangeEvent = onValueChange + ? (event: Event) => { + let userEvent = true; + if (Platform.OS === 'android') { + // On Android there's a special flag telling us the user is + // dragging the slider. + userEvent = + event.nativeEvent.fromUser != null && event.nativeEvent.fromUser; + } + userEvent && onValueChange(event.nativeEvent.value); } - props.onValueChange && - userEvent && - props.onValueChange(event.nativeEvent.value); - }); - - const onChange = onValueChange; + : null; - const onSlidingComplete = - props.onSlidingComplete && - ((event: Event) => { - props.onSlidingComplete && - props.onSlidingComplete(event.nativeEvent.value); - }); + const onChangeEvent = onValueChangeEvent; + const onSlidingCompleteEvent = onSlidingComplete + ? (event: Event) => { + onSlidingComplete(event.nativeEvent.value); + } + : null; return ( - true} onResponderTerminationRequest={() => false} @@ -240,9 +241,11 @@ const Slider = ( ); }; -// $FlowFixMe - TODO T29156721 `React.forwardRef` is not defined in Flow, yet. const SliderWithRef = React.forwardRef(Slider); +/* $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. */ SliderWithRef.defaultProps = { disabled: false, value: 0, @@ -264,4 +267,7 @@ if (Platform.OS === 'ios') { }); } +/* $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. */ module.exports = (SliderWithRef: Class>); diff --git a/Libraries/Components/StaticContainer.react.js b/Libraries/Components/StaticContainer.react.js index ffba809a46ab72..4772dd4af482cc 100644 --- a/Libraries/Components/StaticContainer.react.js +++ b/Libraries/Components/StaticContainer.react.js @@ -1,11 +1,11 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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 + * @flow strict-local */ 'use strict'; @@ -27,8 +27,19 @@ const React = require('React'); * Typically, you will not need to use this component and should opt for normal * React reconciliation. */ -class StaticContainer extends React.Component { - shouldComponentUpdate(nextProps: Object): boolean { + +type Props = $ReadOnly<{| + /** + * Whether or not this component should update. + */ + shouldUpdate: ?boolean, + /** + * Content short-circuited by React reconciliation process. + */ + children: React.Node, +|}>; +class StaticContainer extends React.Component { + shouldComponentUpdate(nextProps: Props): boolean { return !!nextProps.shouldUpdate; } diff --git a/Libraries/Components/StaticRenderer.js b/Libraries/Components/StaticRenderer.js index 4f8cfbd0989fa8..f4d630d6a98ab0 100644 --- a/Libraries/Components/StaticRenderer.js +++ b/Libraries/Components/StaticRenderer.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -12,18 +12,20 @@ const React = require('React'); -const PropTypes = require('prop-types'); - -class StaticRenderer extends React.Component<{ +type Props = $ReadOnly<{| + /** + * Indicates whether the render function needs to be called again + */ shouldUpdate: boolean, - render: Function, -}> { - static propTypes = { - shouldUpdate: PropTypes.bool.isRequired, - render: PropTypes.func.isRequired, - }; + /** + * () => renderable + * A function that returns a renderable component + */ + render: () => React.Node, +|}>; - shouldComponentUpdate(nextProps: {shouldUpdate: boolean}): boolean { +class StaticRenderer extends React.Component { + shouldComponentUpdate(nextProps: Props): boolean { return nextProps.shouldUpdate; } diff --git a/Libraries/Components/StatusBar/StatusBar.js b/Libraries/Components/StatusBar/StatusBar.js index 97db49fdd10283..0ee780d25bdb99 100644 --- a/Libraries/Components/StatusBar/StatusBar.js +++ b/Libraries/Components/StatusBar/StatusBar.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -11,8 +11,6 @@ 'use strict'; const React = require('React'); -const PropTypes = require('prop-types'); -const ColorPropType = require('ColorPropType'); const Platform = require('Platform'); const processColor = require('processColor'); @@ -55,9 +53,55 @@ export type StatusBarAnimation = $Enum<{ slide: string, }>; -type DefaultProps = { - animated: boolean, -}; +type AndroidProps = $ReadOnly<{| + /** + * The background color of the status bar. + * @platform android + */ + backgroundColor?: ?string, + /** + * If the status bar is translucent. + * When translucent is set to true, the app will draw under the status bar. + * This is useful when using a semi transparent status bar color. + * + * @platform android + */ + translucent?: ?boolean, +|}>; + +type IOSProps = $ReadOnly<{| + /** + * If the network activity indicator should be visible. + * + * @platform ios + */ + networkActivityIndicatorVisible?: ?boolean, + /** + * The transition effect when showing and hiding the status bar using the `hidden` + * prop. Defaults to 'fade'. + * + * @platform ios + */ + showHideTransition?: ?('fade' | 'slide'), +|}>; + +type Props = $ReadOnly<{| + ...AndroidProps, + ...IOSProps, + /** + * If the status bar is hidden. + */ + hidden?: ?boolean, + /** + * If the transition between status bar property changes should be animated. + * Supported for backgroundColor, barStyle and hidden. + */ + animated?: ?boolean, + /** + * Sets the color of the status bar text. + */ + barStyle?: ?('default' | 'light-content' | 'dark-content'), +|}>; /** * Merges the prop stack with the default values. @@ -138,31 +182,49 @@ function createStackEntry(props: any): any { * * ### Imperative API * - * For cases where using a component is not ideal, there is also an imperative - * API exposed as static functions on the component. It is however not recommended - * to use the static API and the component for the same prop because any value - * set by the static API will get overriden by the one set by the component in - * the next render. + * For cases where using a component is not ideal, there are static methods + * to manipulate the `StatusBar` display stack. These methods have the same + * behavior as mounting and unmounting a `StatusBar` component. + * + * For example, you can call `StatusBar.pushStackEntry` to update the status bar + * before launching a third-party native UI component, and then call + * `StatusBar.popStackEntry` when completed. + * + * ``` + * const openThirdPartyBugReporter = async () => { + * // The bug reporter has a dark background, so we push a new status bar style. + * const stackEntry = StatusBar.pushStackEntry({barStyle: 'light-content'}); + * + * // `open` returns a promise that resolves when the UI is dismissed. + * await BugReporter.open(); + * + * // Don't forget to call `popStackEntry` when you're done. + * StatusBar.popStackEntry(stackEntry); + * }; + * ``` + * + * There is a legacy imperative API that enables you to manually update the + * status bar styles. However, the legacy API does not update the internal + * `StatusBar` display stack, which means that any changes will be overridden + * whenever a `StatusBar` component is mounted or unmounted. + * + * It is strongly advised that you use `pushStackEntry`, `popStackEntry`, or + * `replaceStackEntry` instead of the static methods beginning with `set`. * * ### Constants * * `currentHeight` (Android only) The height of the status bar. */ -class StatusBar extends React.Component<{ - hidden?: boolean, - animated?: boolean, - backgroundColor?: string, - translucent?: boolean, - barStyle?: 'default' | 'light-content' | 'dark-content', - networkActivityIndicatorVisible?: boolean, - showHideTransition?: 'fade' | 'slide', -}> { +class StatusBar extends React.Component { static _propsStack = []; static _defaultProps = createStackEntry({ animated: false, showHideTransition: 'fade', - backgroundColor: 'black', + backgroundColor: Platform.select({ + android: StatusBarManager.DEFAULT_BACKGROUND_COLOR ?? 'black', + ios: 'black', + }), barStyle: 'default', translucent: false, hidden: false, @@ -261,47 +323,47 @@ class StatusBar extends React.Component<{ StatusBarManager.setTranslucent(translucent); } - static propTypes = { - /** - * If the status bar is hidden. - */ - hidden: PropTypes.bool, - /** - * If the transition between status bar property changes should be animated. - * Supported for backgroundColor, barStyle and hidden. - */ - animated: PropTypes.bool, - /** - * The background color of the status bar. - * @platform android - */ - backgroundColor: ColorPropType, - /** - * If the status bar is translucent. - * When translucent is set to true, the app will draw under the status bar. - * This is useful when using a semi transparent status bar color. - * - * @platform android - */ - translucent: PropTypes.bool, - /** - * Sets the color of the status bar text. - */ - barStyle: PropTypes.oneOf(['default', 'light-content', 'dark-content']), - /** - * If the network activity indicator should be visible. - * - * @platform ios - */ - networkActivityIndicatorVisible: PropTypes.bool, - /** - * The transition effect when showing and hiding the status bar using the `hidden` - * prop. Defaults to 'fade'. - * - * @platform ios - */ - showHideTransition: PropTypes.oneOf(['fade', 'slide']), - }; + /** + * Push a StatusBar entry onto the stack. + * The return value should be passed to `popStackEntry` when complete. + * + * @param props Object containing the StatusBar props to use in the stack entry. + */ + static pushStackEntry(props: any) { + const entry = createStackEntry(props); + StatusBar._propsStack.push(entry); + StatusBar._updatePropsStack(); + return entry; + } + + /** + * Pop a StatusBar entry from the stack. + * + * @param entry Entry returned from `pushStackEntry`. + */ + static popStackEntry(entry: any) { + const index = StatusBar._propsStack.indexOf(entry); + if (index !== -1) { + StatusBar._propsStack.splice(index, 1); + } + StatusBar._updatePropsStack(); + } + + /** + * Replace an existing StatusBar stack entry with new props. + * + * @param entry Entry returned from `pushStackEntry` to replace. + * @param props Object containing the StatusBar props to use in the replacement stack entry. + */ + static replaceStackEntry(entry: any, props: any) { + const newEntry = createStackEntry(props); + const index = StatusBar._propsStack.indexOf(entry); + if (index !== -1) { + StatusBar._propsStack[index] = newEntry; + } + StatusBar._updatePropsStack(); + return newEntry; + } static defaultProps = { animated: false, @@ -314,33 +376,27 @@ class StatusBar extends React.Component<{ // 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 - // added last or is deeper in the view hierarchy will have priority. - this._stackEntry = createStackEntry(this.props); - StatusBar._propsStack.push(this._stackEntry); - this._updatePropsStack(); + // added last or is deeper in the view hierachy will have priority. + this._stackEntry = StatusBar.pushStackEntry(this.props); } componentWillUnmount() { // When a StatusBar is unmounted, remove itself from the stack and update // the native bar with the next props. - const index = StatusBar._propsStack.indexOf(this._stackEntry); - StatusBar._propsStack.splice(index, 1); - - this._updatePropsStack(); + StatusBar.popStackEntry(this._stackEntry); } componentDidUpdate() { - const index = StatusBar._propsStack.indexOf(this._stackEntry); - this._stackEntry = createStackEntry(this.props); - StatusBar._propsStack[index] = this._stackEntry; - - this._updatePropsStack(); + this._stackEntry = StatusBar.replaceStackEntry( + this._stackEntry, + this.props, + ); } /** * Updates the native status bar with the props from the stack. */ - _updatePropsStack = () => { + static _updatePropsStack = () => { // Send the update to the native module only once at the end of the frame. clearImmediate(StatusBar._updateImmediate); StatusBar._updateImmediate = setImmediate(() => { @@ -358,7 +414,7 @@ class StatusBar extends React.Component<{ ) { StatusBarManager.setStyle( mergedProps.barStyle.value, - mergedProps.barStyle.animated, + mergedProps.barStyle.animated || false, ); } if (!oldProps || oldProps.hidden.value !== mergedProps.hidden.value) { diff --git a/Libraries/Components/StatusBar/StatusBarIOS.android.js b/Libraries/Components/StatusBar/StatusBarIOS.android.js index fc8b4b70dc70d0..4e9f97309b29b0 100644 --- a/Libraries/Components/StatusBar/StatusBarIOS.android.js +++ b/Libraries/Components/StatusBar/StatusBarIOS.android.js @@ -1,15 +1,17 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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 + * @flow */ 'use strict'; const NativeEventEmitter = require('NativeEventEmitter'); +/* $FlowFixMe(>=0.78.0 site=react_native_android_fb) This issue was found when + * making Flow check .android.js files. */ module.exports = new NativeEventEmitter('StatusBarManager'); diff --git a/Libraries/Components/StatusBar/StatusBarIOS.ios.js b/Libraries/Components/StatusBar/StatusBarIOS.ios.js index 529e01b308fd88..ea558eafa9ba94 100644 --- a/Libraries/Components/StatusBar/StatusBarIOS.ios.js +++ b/Libraries/Components/StatusBar/StatusBarIOS.ios.js @@ -1,11 +1,11 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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 + * @flow strict-local */ 'use strict'; diff --git a/Libraries/Components/Subscribable.js b/Libraries/Components/Subscribable.js deleted file mode 100644 index a56d42ed864c8d..00000000000000 --- a/Libraries/Components/Subscribable.js +++ /dev/null @@ -1,65 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * - * 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 type EventEmitter from 'EventEmitter'; - -/** - * Subscribable provides a mixin for safely subscribing a component to an - * eventEmitter - * - * This will be replaced with the observe interface that will be coming soon to - * React Core - */ - -const Subscribable = {}; - -Subscribable.Mixin = { - UNSAFE_componentWillMount: function() { - this._subscribableSubscriptions = []; - }, - - componentWillUnmount: function() { - // This null check is a fix for a broken version of uglify-es. Should be deleted eventually - // https://github.com/facebook/react-native/issues/17348 - this._subscribableSubscriptions && - this._subscribableSubscriptions.forEach(subscription => - subscription.remove(), - ); - this._subscribableSubscriptions = null; - }, - - /** - * Special form of calling `addListener` that *guarantees* that a - * subscription *must* be tied to a component instance, and therefore will - * be cleaned up when the component is unmounted. It is impossible to create - * the subscription and pass it in - this method must be the one to create - * the subscription and therefore can guarantee it is retained in a way that - * will be cleaned up. - * - * @param {EventEmitter} eventEmitter emitter to subscribe to. - * @param {string} eventType Type of event to listen to. - * @param {function} listener Function to invoke when event occurs. - * @param {object} context Object to use as listener context. - */ - addListenerOn: function( - eventEmitter: EventEmitter, - eventType: string, - listener: Function, - context: Object, - ) { - this._subscribableSubscriptions.push( - eventEmitter.addListener(eventType, listener, context), - ); - }, -}; - -module.exports = Subscribable; diff --git a/Libraries/Components/Switch/Switch.js b/Libraries/Components/Switch/Switch.js index 237519a5e0b579..ad928639748fd9 100644 --- a/Libraries/Components/Switch/Switch.js +++ b/Libraries/Components/Switch/Switch.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2013-present, Facebook, Inc. + * 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. @@ -10,16 +10,15 @@ 'use strict'; +const SwitchNativeComponent = require('SwitchNativeComponent'); const Platform = require('Platform'); const React = require('React'); -const ReactNative = require('ReactNative'); const StyleSheet = require('StyleSheet'); -const requireNativeComponent = require('requireNativeComponent'); - import type {SwitchChangeEvent} from 'CoreEventTypes'; import type {ColorValue} from 'StyleSheetTypes'; import type {ViewProps} from 'ViewPropTypes'; +import type {NativeAndroidProps, NativeIOSProps} from 'SwitchNativeComponent'; export type Props = $ReadOnly<{| ...ViewProps, @@ -73,48 +72,8 @@ export type Props = $ReadOnly<{| * event, use `onChange`. */ onValueChange?: ?(value: boolean) => Promise | void, - - /** - * Identifier used to find this view in tests. - */ - testID?: ?string, -|}>; - -// @see ReactSwitchManager.java -type NativeAndroidProps = $ReadOnly<{| - ...ViewProps, - enabled?: ?boolean, - on?: ?boolean, - onChange?: ?(event: SwitchChangeEvent) => mixed, - thumbTintColor?: ?string, - trackTintColor?: ?string, |}>; -// @see RCTSwitchManager.m -type NativeIOSProps = $ReadOnly<{| - ...ViewProps, - disabled?: ?boolean, - onChange?: ?(event: SwitchChangeEvent) => mixed, - onTintColor?: ?string, - thumbTintColor?: ?string, - tintColor?: ?string, - value?: ?boolean, -|}>; - -type NativeSwitchType = Class< - ReactNative.NativeComponent< - $ReadOnly<{| - ...NativeAndroidProps, - ...NativeIOSProps, - |}>, - >, ->; - -const NativeSwitch: NativeSwitchType = - Platform.OS === 'android' - ? (requireNativeComponent('AndroidSwitch'): any) - : (requireNativeComponent('RCTSwitch'): any); - /** * A visual toggle between two mutually exclusive states. * @@ -124,7 +83,7 @@ const NativeSwitch: NativeSwitchType = * supplied `value` prop instead of the expected result of any user actions. */ class Switch extends React.Component { - _nativeSwitchRef: ?React.ElementRef; + _nativeSwitchRef: ?React.ElementRef; render() { const { @@ -133,7 +92,6 @@ class Switch extends React.Component { onChange, onValueChange, style, - testID, thumbColor, trackColor, value, @@ -151,7 +109,7 @@ class Switch extends React.Component { _thumbColor = thumbTintColor; if (__DEV__) { console.warn( - 'Switch: `thumbTintColor` is deprecated, use `_thumbColor` instead.', + 'Switch: `thumbTintColor` is deprecated, use `thumbColor` instead.', ); } } @@ -179,6 +137,8 @@ class Switch extends React.Component { on: value === true, style, thumbTintColor: _thumbColor, + trackColorForFalse: _trackColorForFalse, + trackColorForTrue: _trackColorForTrue, trackTintColor: value === true ? _trackColorForTrue : _trackColorForFalse, }: NativeAndroidProps) @@ -203,13 +163,14 @@ class Switch extends React.Component { }: NativeIOSProps); return ( - ); } @@ -236,7 +197,9 @@ class Switch extends React.Component { } }; - _handleNativeSwitchRef = (ref: ?React.ElementRef) => { + _handleSwitchNativeComponentRef = ( + ref: ?React.ElementRef, + ) => { this._nativeSwitchRef = ref; }; } diff --git a/Libraries/Components/Switch/SwitchNativeComponent.js b/Libraries/Components/Switch/SwitchNativeComponent.js new file mode 100644 index 00000000000000..fd46bf64ea236d --- /dev/null +++ b/Libraries/Components/Switch/SwitchNativeComponent.js @@ -0,0 +1,64 @@ +/** + * 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 + * @format + */ + +'use strict'; + +const Platform = require('Platform'); +const ReactNative = require('ReactNative'); + +const requireNativeComponent = require('requireNativeComponent'); + +import type {SwitchChangeEvent} from 'CoreEventTypes'; +import type {ViewProps} from 'ViewPropTypes'; + +type SwitchProps = $ReadOnly<{| + ...ViewProps, + disabled?: ?boolean, + onChange?: ?(event: SwitchChangeEvent) => mixed, + thumbColor?: ?string, + trackColorForFalse?: ?string, + trackColorForTrue?: ?string, + value?: ?boolean, +|}>; + +// @see ReactSwitchManager.java +export type NativeAndroidProps = $ReadOnly<{| + ...SwitchProps, + + enabled?: ?boolean, + on?: ?boolean, + thumbTintColor?: ?string, + trackTintColor?: ?string, +|}>; + +// @see RCTSwitchManager.m +export type NativeIOSProps = $ReadOnly<{| + ...SwitchProps, + + onTintColor?: ?string, + thumbTintColor?: ?string, + tintColor?: ?string, +|}>; + +type SwitchNativeComponentType = Class< + ReactNative.NativeComponent< + $ReadOnly<{| + ...NativeAndroidProps, + ...NativeIOSProps, + |}>, + >, +>; + +const SwitchNativeComponent: SwitchNativeComponentType = + Platform.OS === 'android' + ? (requireNativeComponent('AndroidSwitch'): any) + : (requireNativeComponent('RCTSwitch'): any); + +module.exports = SwitchNativeComponent; diff --git a/Libraries/Components/Switch/SwitchSchema.js b/Libraries/Components/Switch/SwitchSchema.js new file mode 100644 index 00000000000000..20adba0aefc913 --- /dev/null +++ b/Libraries/Components/Switch/SwitchSchema.js @@ -0,0 +1,94 @@ +/** + * 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 type {SchemaType} from '../../../packages/react-native-codegen/src/CodegenSchema.js'; + +const SwitchSchema: SchemaType = { + modules: { + Switch: { + components: { + Switch: { + extendsProps: [ + { + type: 'ReactNativeBuiltInType', + knownTypeName: 'ReactNativeCoreViewProps', + }, + ], + events: [ + { + name: 'onChange', + optional: true, + bubblingType: 'bubble', + typeAnnotation: { + type: 'EventTypeAnnotation', + argument: { + type: 'ObjectTypeAnnotation', + properties: [ + { + type: 'BooleanTypeAnnotation', + name: 'value', + optional: false, + }, + ], + }, + }, + }, + ], + props: [ + { + name: 'disabled', + optional: true, + typeAnnotation: { + type: 'BooleanTypeAnnotation', + default: false, + }, + }, + { + name: 'value', + optional: true, + typeAnnotation: { + type: 'BooleanTypeAnnotation', + default: false, + }, + }, + { + name: 'tintColor', + optional: true, + typeAnnotation: { + type: 'NativePrimitiveTypeAnnotation', + name: 'ColorPrimitive', + }, + }, + { + name: 'onTintColor', + optional: true, + typeAnnotation: { + type: 'NativePrimitiveTypeAnnotation', + name: 'ColorPrimitive', + }, + }, + { + name: 'thumbTintColor', + optional: true, + typeAnnotation: { + type: 'NativePrimitiveTypeAnnotation', + name: 'ColorPrimitive', + }, + }, + ], + }, + }, + }, + }, +}; + +module.exports = SwitchSchema; diff --git a/Libraries/Components/TabBarIOS/TabBarIOS.android.js b/Libraries/Components/TabBarIOS/TabBarIOS.android.js deleted file mode 100644 index 06dfe10dbc2b7f..00000000000000 --- a/Libraries/Components/TabBarIOS/TabBarIOS.android.js +++ /dev/null @@ -1,36 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * - * 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 - */ - -'use strict'; - -const React = require('React'); -const StyleSheet = require('StyleSheet'); -const TabBarItemIOS = require('TabBarItemIOS'); -const View = require('View'); - -class DummyTabBarIOS extends React.Component<$FlowFixMeProps> { - static Item = TabBarItemIOS; - - render() { - return ( - - {this.props.children} - - ); - } -} - -const styles = StyleSheet.create({ - tabGroup: { - flex: 1, - }, -}); - -module.exports = DummyTabBarIOS; diff --git a/Libraries/Components/TabBarIOS/TabBarIOS.ios.js b/Libraries/Components/TabBarIOS/TabBarIOS.ios.js deleted file mode 100644 index 6cbe213f7fc36d..00000000000000 --- a/Libraries/Components/TabBarIOS/TabBarIOS.ios.js +++ /dev/null @@ -1,107 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * - * 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 ColorPropType = require('ColorPropType'); -const React = require('React'); -const PropTypes = require('prop-types'); -const StyleSheet = require('StyleSheet'); -const TabBarItemIOS = require('TabBarItemIOS'); -const ViewPropTypes = require('ViewPropTypes'); - -const requireNativeComponent = require('requireNativeComponent'); - -import type {DangerouslyImpreciseStyleProp} from 'StyleSheet'; -import type {ViewProps} from 'ViewPropTypes'; - -const RCTTabBar = requireNativeComponent('RCTTabBar'); - -type Props = $ReadOnly<{| - ...ViewProps, - style?: DangerouslyImpreciseStyleProp, - unselectedTintColor?: string, - tintColor?: string, - unselectedItemTintColor?: string, - barTintColor?: string, - barStyle?: 'default' | 'black', - translucent?: boolean, - itemPositioning?: 'fill' | 'center' | 'auto', - children: React.Node, -|}>; - -class TabBarIOS extends React.Component { - static Item = TabBarItemIOS; - - static propTypes = { - ...ViewPropTypes, - style: ViewPropTypes.style, - /** - * Color of text on unselected tabs - */ - unselectedTintColor: ColorPropType, - /** - * Color of the currently selected tab icon - */ - tintColor: ColorPropType, - /** - * Color of unselected tab icons. Available since iOS 10. - */ - unselectedItemTintColor: ColorPropType, - /** - * Background color of the tab bar - */ - barTintColor: ColorPropType, - /** - * The style of the tab bar. Supported values are 'default', 'black'. - * Use 'black' instead of setting `barTintColor` to black. This produces - * a tab bar with the native iOS style with higher translucency. - */ - barStyle: PropTypes.oneOf(['default', 'black']), - /** - * A Boolean value that indicates whether the tab bar is translucent - */ - translucent: PropTypes.bool, - /** - * Specifies tab bar item positioning. Available values are: - * - fill - distributes items across the entire width of the tab bar - * - center - centers item in the available tab bar space - * - auto (default) - distributes items dynamically according to the - * user interface idiom. In a horizontally compact environment (e.g. iPhone 5) - * this value defaults to `fill`, in a horizontally regular one (e.g. iPad) - * it defaults to center. - */ - itemPositioning: PropTypes.oneOf(['fill', 'center', 'auto']), - }; - - render() { - return ( - - {this.props.children} - - ); - } -} - -const styles = StyleSheet.create({ - tabGroup: { - flex: 1, - }, -}); - -module.exports = TabBarIOS; diff --git a/Libraries/Components/TabBarIOS/TabBarItemIOS.android.js b/Libraries/Components/TabBarIOS/TabBarItemIOS.android.js deleted file mode 100644 index 37dc72f4d41faf..00000000000000 --- a/Libraries/Components/TabBarIOS/TabBarItemIOS.android.js +++ /dev/null @@ -1,40 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @format - */ - -'use strict'; - -const React = require('React'); -const View = require('View'); -const StyleSheet = require('StyleSheet'); - -class DummyTab extends React.Component { - render() { - if (!this.props.selected) { - return ; - } - return ( - {this.props.children} - ); - } -} - -const styles = StyleSheet.create({ - tab: { - // TODO(5405356): Implement overflow: visible so position: absolute isn't useless - // position: 'absolute', - top: 0, - right: 0, - bottom: 0, - left: 0, - borderColor: 'red', - borderWidth: 1, - }, -}); - -module.exports = DummyTab; diff --git a/Libraries/Components/TabBarIOS/TabBarItemIOS.ios.js b/Libraries/Components/TabBarIOS/TabBarItemIOS.ios.js deleted file mode 100644 index b8825977a8cac6..00000000000000 --- a/Libraries/Components/TabBarIOS/TabBarItemIOS.ios.js +++ /dev/null @@ -1,148 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @format - * @noflow - */ - -'use strict'; - -const ColorPropType = require('ColorPropType'); -const Image = require('Image'); -const React = require('React'); -const PropTypes = require('prop-types'); -const StaticContainer = require('StaticContainer.react'); -const StyleSheet = require('StyleSheet'); -const View = require('View'); - -const ViewPropTypes = require('ViewPropTypes'); - -const requireNativeComponent = require('requireNativeComponent'); - -class TabBarItemIOS extends React.Component { - static propTypes = { - ...ViewPropTypes, - /** - * Little red bubble that sits at the top right of the icon. - */ - badge: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), - /** - * Background color for the badge. Available since iOS 10. - */ - badgeColor: ColorPropType, - /** - * Items comes with a few predefined system icons. Note that if you are - * using them, the title and selectedIcon will be overridden with the - * system ones. - */ - systemIcon: PropTypes.oneOf([ - 'bookmarks', - 'contacts', - 'downloads', - 'favorites', - 'featured', - 'history', - 'more', - 'most-recent', - 'most-viewed', - 'recents', - 'search', - 'top-rated', - ]), - /** - * A custom icon for the tab. It is ignored when a system icon is defined. - */ - icon: Image.propTypes.source, - /** - * A custom icon when the tab is selected. It is ignored when a system - * icon is defined. If left empty, the icon will be tinted in blue. - */ - selectedIcon: Image.propTypes.source, - /** - * Callback when this tab is being selected, you should change the state of your - * component to set selected={true}. - */ - onPress: PropTypes.func, - /** - * If set to true it renders the image as original, - * it defaults to being displayed as a template - */ - renderAsOriginal: PropTypes.bool, - /** - * It specifies whether the children are visible or not. If you see a - * blank content, you probably forgot to add a selected one. - */ - selected: PropTypes.bool, - /** - * React style object. - */ - style: ViewPropTypes.style, - /** - * Text that appears under the icon. It is ignored when a system icon - * is defined. - */ - title: PropTypes.string, - /** - *(Apple TV only)* When set to true, this view will be focusable - * and navigable using the Apple TV remote. - * - * @platform ios - */ - isTVSelectable: PropTypes.bool, - }; - - state = { - hasBeenSelected: false, - }; - - UNSAFE_componentWillMount() { - if (this.props.selected) { - this.setState({hasBeenSelected: true}); - } - } - - UNSAFE_componentWillReceiveProps(nextProps: {selected?: boolean}) { - if (this.state.hasBeenSelected || nextProps.selected) { - this.setState({hasBeenSelected: true}); - } - } - - render() { - const {style, children, ...props} = this.props; - - // if the tab has already been shown once, always continue to show it so we - // preserve state between tab transitions - if (this.state.hasBeenSelected) { - var tabContents = ( - - {children} - - ); - } else { - var tabContents = ; - } - - return ( - - {tabContents} - - ); - } -} - -const styles = StyleSheet.create({ - tab: { - position: 'absolute', - top: 0, - right: 0, - bottom: 0, - left: 0, - }, -}); - -const RCTTabBarItem = requireNativeComponent('RCTTabBarItem'); - -module.exports = TabBarItemIOS; diff --git a/Libraries/Components/TextInput/InputAccessoryView.js b/Libraries/Components/TextInput/InputAccessoryView.js index 731476bc7138b7..1272dc0f1cc20c 100644 --- a/Libraries/Components/TextInput/InputAccessoryView.js +++ b/Libraries/Components/TextInput/InputAccessoryView.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2013-present, Facebook, Inc. + * 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. @@ -9,14 +9,14 @@ */ 'use strict'; -const ColorPropType = require('ColorPropType'); +const DeprecatedColorPropType = require('DeprecatedColorPropType'); +const Platform = require('Platform'); const React = require('React'); const StyleSheet = require('StyleSheet'); -const ViewPropTypes = require('ViewPropTypes'); -const requireNativeComponent = require('requireNativeComponent'); +const RCTInputAccessoryViewNativeComponent = require('RCTInputAccessoryViewNativeComponent'); -const RCTInputAccessoryView = requireNativeComponent('RCTInputAccessoryView'); +import type {ViewStyleProp} from 'StyleSheet'; /** * Note: iOS only @@ -76,32 +76,34 @@ const RCTInputAccessoryView = requireNativeComponent('RCTInputAccessoryView'); * For an example, look at InputAccessoryViewExample.js in RNTester. */ -type Props = { +type Props = $ReadOnly<{| +children: React.Node, /** * An ID which is used to associate this `InputAccessoryView` to * specified TextInput(s). */ - nativeID?: string, - style?: ViewPropTypes.style, - backgroundColor?: ColorPropType, -}; + nativeID?: ?string, + style?: ?ViewStyleProp, + backgroundColor?: ?DeprecatedColorPropType, +|}>; class InputAccessoryView extends React.Component { render(): React.Node { - console.warn(' is not supported on Android yet.'); + if (Platform.OS !== 'ios') { + console.warn(' is only supported on iOS.'); + } if (React.Children.count(this.props.children) === 0) { return null; } return ( - {this.props.children} - + ); } } diff --git a/Libraries/Components/TextInput/RCTInputAccessoryViewNativeComponent.js b/Libraries/Components/TextInput/RCTInputAccessoryViewNativeComponent.js new file mode 100644 index 00000000000000..6a220e9b468ac7 --- /dev/null +++ b/Libraries/Components/TextInput/RCTInputAccessoryViewNativeComponent.js @@ -0,0 +1,34 @@ +/** + * 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 + * @format + */ +'use strict'; + +import type {NativeComponent} from 'ReactNative'; +import type {ColorValue} from 'StyleSheetTypes'; +import type {ViewStyleProp} from 'StyleSheet'; + +const React = require('React'); +const requireNativeComponent = require('requireNativeComponent'); + +type NativeProps = $ReadOnly<{| + +children: React.Node, + /** + * An ID which is used to associate this `InputAccessoryView` to + * specified TextInput(s). + */ + nativeID?: ?string, + style?: ?ViewStyleProp, + backgroundColor?: ?ColorValue, +|}>; + +type NativeInputAccessoryView = Class>; + +module.exports = ((requireNativeComponent( + 'RCTInputAccessoryView', +): any): NativeInputAccessoryView); diff --git a/Libraries/Components/TextInput/TextInput.js b/Libraries/Components/TextInput/TextInput.js index 80dcb8ecf182a2..296b6ec72dc029 100644 --- a/Libraries/Components/TextInput/TextInput.js +++ b/Libraries/Components/TextInput/TextInput.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -9,32 +9,31 @@ */ 'use strict'; -const ColorPropType = require('ColorPropType'); +const DeprecatedColorPropType = require('DeprecatedColorPropType'); +const DeprecatedViewPropTypes = require('DeprecatedViewPropTypes'); const DocumentSelectionState = require('DocumentSelectionState'); -const EventEmitter = require('EventEmitter'); const NativeMethodsMixin = require('NativeMethodsMixin'); const Platform = require('Platform'); -const React = require('React'); -const createReactClass = require('create-react-class'); const PropTypes = require('prop-types'); +const React = require('React'); const ReactNative = require('ReactNative'); const StyleSheet = require('StyleSheet'); const Text = require('Text'); const TextAncestor = require('TextAncestor'); const TextInputState = require('TextInputState'); -const TimerMixin = require('react-timer-mixin'); const TouchableWithoutFeedback = require('TouchableWithoutFeedback'); const UIManager = require('UIManager'); -const ViewPropTypes = require('ViewPropTypes'); -const emptyFunction = require('fbjs/lib/emptyFunction'); -const invariant = require('fbjs/lib/invariant'); +const createReactClass = require('create-react-class'); +const invariant = require('invariant'); const requireNativeComponent = require('requireNativeComponent'); const warning = require('fbjs/lib/warning'); +import type {TextStyleProp, ViewStyleProp} from 'StyleSheet'; import type {ColorValue} from 'StyleSheetTypes'; -import type {TextStyleProp} from 'StyleSheet'; import type {ViewProps} from 'ViewPropTypes'; +import type {SyntheticEvent, ScrollEvent} from 'CoreEventTypes'; +import type {PressEvent} from 'CoreEventTypes'; let AndroidTextInput; let RCTMultilineTextInputView; @@ -56,11 +55,73 @@ const onlyMultiline = { children: true, }; -type Event = Object; -type Selection = { +export type ChangeEvent = SyntheticEvent< + $ReadOnly<{| + eventCount: number, + target: number, + text: string, + |}>, +>; + +export type TextInputEvent = SyntheticEvent< + $ReadOnly<{| + eventCount: number, + previousText: string, + range: $ReadOnly<{| + start: number, + end: number, + |}>, + target: number, + text: string, + |}>, +>; + +export type ContentSizeChangeEvent = SyntheticEvent< + $ReadOnly<{| + target: number, + contentSize: $ReadOnly<{| + width: number, + height: number, + |}>, + |}>, +>; + +type TargetEvent = SyntheticEvent< + $ReadOnly<{| + target: number, + |}>, +>; + +export type BlurEvent = TargetEvent; +export type FocusEvent = TargetEvent; + +type Selection = $ReadOnly<{| start: number, - end?: number, -}; + end: number, +|}>; + +export type SelectionChangeEvent = SyntheticEvent< + $ReadOnly<{| + selection: Selection, + target: number, + |}>, +>; + +export type KeyPressEvent = SyntheticEvent< + $ReadOnly<{| + key: string, + target?: ?number, + eventCount?: ?number, + |}>, +>; + +export type EditingEvent = SyntheticEvent< + $ReadOnly<{| + eventCount: number, + text: string, + target: number, + |}>, +>; const DataDetectorTypes = [ 'phoneNumber', @@ -155,7 +216,10 @@ type IOSProps = $ReadOnly<{| | 'telephoneNumber' | 'username' | 'password' + | 'newPassword' + | 'oneTimeCode' ), + scrollEnabled?: ?boolean, |}>; type AndroidProps = $ReadOnly<{| @@ -169,29 +233,30 @@ type AndroidProps = $ReadOnly<{| |}>; type Props = $ReadOnly<{| - ...ViewProps, + ...$Diff>, ...IOSProps, ...AndroidProps, autoCapitalize?: ?AutoCapitalize, autoCorrect?: ?boolean, autoFocus?: ?boolean, allowFontScaling?: ?boolean, + maxFontSizeMultiplier?: ?number, editable?: ?boolean, keyboardType?: ?KeyboardType, returnKeyType?: ?ReturnKeyType, maxLength?: ?number, multiline?: ?boolean, - onBlur?: ?Function, - onFocus?: ?Function, - onChange?: ?Function, - onChangeText?: ?Function, - onContentSizeChange?: ?Function, - onTextInput?: ?Function, - onEndEditing?: ?Function, - onSelectionChange?: ?Function, - onSubmitEditing?: ?Function, - onKeyPress?: ?Function, - onScroll?: ?Function, + onBlur?: ?(e: BlurEvent) => mixed, + onFocus?: ?(e: FocusEvent) => mixed, + onChange?: ?(e: ChangeEvent) => mixed, + onChangeText?: ?(text: string) => mixed, + onContentSizeChange?: ?(e: ContentSizeChangeEvent) => mixed, + onTextInput?: ?(e: TextInputEvent) => mixed, + onEndEditing?: ?(e: EditingEvent) => mixed, + onSelectionChange?: ?(e: SelectionChangeEvent) => mixed, + onSubmitEditing?: ?(e: EditingEvent) => mixed, + onKeyPress?: ?(e: KeyPressEvent) => mixed, + onScroll?: ?(e: ScrollEvent) => mixed, placeholder?: ?Stringish, placeholderTextColor?: ?ColorValue, secureTextEntry?: ?boolean, @@ -209,6 +274,8 @@ type Props = $ReadOnly<{| contextMenuHidden?: ?boolean, |}>; +const emptyFunctionThatReturnsTrue = () => true; + /** * A foundational component for inputting text into the app via a * keyboard. Props provide configurability for several features, such as @@ -326,22 +393,12 @@ const TextInput = createReactClass({ statics: { State: { currentlyFocusedField: TextInputState.currentlyFocusedField, - focusTextInput: (textFieldID: ?number) => { - console.warn( - '`focusTextInput` is deprecated, use the `focus` method of the `TextInput` ref instead.', - ); - TextInputState.focusTextInput(textFieldID); - }, - blurTextInput: (textFieldID: ?number) => { - console.warn( - '`blurTextInput` is deprecated, use `Keyboard.dismiss` or the `blur` method of the `TextInput` ref.', - ); - TextInputState.blurTextInput(textFieldID); - }, + focusTextInput: TextInputState.focusTextInput, + blurTextInput: TextInputState.blurTextInput, }, }, propTypes: { - ...ViewPropTypes, + ...DeprecatedViewPropTypes, /** * Can tell `TextInput` to automatically capitalize certain characters. * @@ -376,6 +433,14 @@ const TextInput = createReactClass({ * default is `true`. */ allowFontScaling: PropTypes.bool, + /** + * Specifies largest possible scale a font can reach when `allowFontScaling` is enabled. + * Possible values: + * `null/undefined` (default): inherit from the parent node or the global default (0) + * `0`: no max, ignore parent/global default + * `>= 1`: sets the maxFontSizeMultiplier of this node to this value + */ + maxFontSizeMultiplier: PropTypes.number, /** * If `false`, text is not editable. The default value is `true`. */ @@ -590,7 +655,13 @@ const TextInput = createReactClass({ /** * The text color of the placeholder string. */ - placeholderTextColor: ColorPropType, + placeholderTextColor: DeprecatedColorPropType, + /** + * If `false`, scrolling of the text view will be disabled. + * The default value is `true`. Does only work with 'multiline={true}'. + * @platform ios + */ + scrollEnabled: PropTypes.bool, /** * If `true`, the text input obscures the text entered so that sensitive text * like passwords stay secure. The default value is `false`. Does not work with 'multiline={true}'. @@ -599,7 +670,7 @@ const TextInput = createReactClass({ /** * The highlight and cursor color of the text input. */ - selectionColor: ColorPropType, + selectionColor: DeprecatedColorPropType, /** * An instance of `DocumentSelectionState`, this is some state that is responsible for * maintaining selection information for a document. @@ -690,7 +761,7 @@ const TextInput = createReactClass({ * The color of the `TextInput` underline. * @platform android */ - underlineColorAndroid: ColorPropType, + underlineColorAndroid: DeprecatedColorPropType, /** * If defined, the provided image resource will be rendered on the left. @@ -711,6 +782,15 @@ const TextInput = createReactClass({ */ inlineImagePadding: PropTypes.number, + /** + * If `true`, allows TextInput to pass touch events to the parent component. + * This allows components such as SwipeableListView to be swipeable from the TextInput on iOS, + * as is the case on Android by default. + * If `false`, TextInput always asks to handle the input (except when disabled). + * @platform ios + */ + rejectResponderTermination: PropTypes.bool, + /** * Determines the types of data converted to clickable URLs in the text input. * Only valid if `multiline={true}` and `editable={false}`. @@ -781,11 +861,14 @@ const TextInput = createReactClass({ 'telephoneNumber', 'username', 'password', + 'newPassword', + 'oneTimeCode', ]), }, - getDefaultProps(): Object { + getDefaultProps() { return { allowFontScaling: true, + rejectResponderTermination: true, underlineColorAndroid: 'transparent', }; }, @@ -793,7 +876,7 @@ const TextInput = createReactClass({ * `NativeMethodsMixin` will look for this when invoking `setNativeProps`. We * make `this` look like an actual native component class. */ - mixins: [NativeMethodsMixin, TimerMixin], + mixins: [NativeMethodsMixin], /** * Returns `true` if the input is currently focused; `false` otherwise. @@ -809,6 +892,7 @@ const TextInput = createReactClass({ _focusSubscription: (undefined: ?Function), _lastNativeText: (undefined: ?string), _lastNativeSelection: (undefined: ?Selection), + _rafId: (null: ?AnimationFrameID), componentDidMount: function() { this._lastNativeText = this.props.value; @@ -818,24 +902,8 @@ const TextInput = createReactClass({ TextInputState.registerInput(tag); } - if (this.context.focusEmitter) { - this._focusSubscription = this.context.focusEmitter.addListener( - 'focus', - el => { - if (this === el) { - this.requestAnimationFrame(this.focus); - } else if (this.isFocused()) { - this.blur(); - } - }, - ); - if (this.props.autoFocus) { - this.context.onFocusRequested(this); - } - } else { - if (this.props.autoFocus) { - this.requestAnimationFrame(this.focus); - } + if (this.props.autoFocus) { + this._rafId = requestAnimationFrame(this.focus); } }, @@ -848,11 +916,9 @@ const TextInput = createReactClass({ if (tag != null) { TextInputState.unregisterInput(tag); } - }, - - contextTypes: { - onFocusRequested: PropTypes.func, - focusEmitter: PropTypes.instanceOf(EventEmitter), + if (this._rafId != null) { + cancelAnimationFrame(this._rafId); + } }, /** @@ -865,7 +931,7 @@ const TextInput = createReactClass({ render: function() { let textInput; if (Platform.OS === 'ios') { - textInput = UIManager.RCTVirtualText + textInput = UIManager.getViewManagerConfig('RCTVirtualText') ? this._renderIOS() : this._renderIOSLegacy(); } else if (Platform.OS === 'android') { @@ -922,7 +988,7 @@ const TextInput = createReactClass({ onBlur={this._onBlur} onChange={this._onChange} onSelectionChange={this._onSelectionChange} - onSelectionChangeShouldSetResponder={emptyFunction.thatReturnsTrue} + onSelectionChangeShouldSetResponder={emptyFunctionThatReturnsTrue} text={this._getText()} /> ); @@ -936,7 +1002,10 @@ const TextInput = createReactClass({ ); if (childCount >= 1) { children = ( - + {children} ); @@ -956,7 +1025,7 @@ const TextInput = createReactClass({ onContentSizeChange={this.props.onContentSizeChange} onSelectionChange={this._onSelectionChange} onTextInput={this._onTextInput} - onSelectionChangeShouldSetResponder={emptyFunction.thatReturnsTrue} + onSelectionChangeShouldSetResponder={emptyFunctionThatReturnsTrue} text={this._getText()} dataDetectorTypes={this.props.dataDetectorTypes} onScroll={this._onScroll} @@ -1009,7 +1078,7 @@ const TextInput = createReactClass({ onContentSizeChange={this.props.onContentSizeChange} onSelectionChange={this._onSelectionChange} onTextInput={this._onTextInput} - onSelectionChangeShouldSetResponder={emptyFunction.thatReturnsTrue} + onSelectionChangeShouldSetResponder={emptyFunctionThatReturnsTrue} text={this._getText()} dataDetectorTypes={this.props.dataDetectorTypes} onScroll={this._onScroll} @@ -1020,7 +1089,7 @@ const TextInput = createReactClass({ =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. */ @@ -1093,7 +1161,7 @@ const TextInput = createReactClass({ ); }, - _onFocus: function(event: Event) { + _onFocus: function(event: FocusEvent) { if (this.props.onFocus) { this.props.onFocus(event); } @@ -1103,16 +1171,16 @@ const TextInput = createReactClass({ } }, - _onPress: function(event: Event) { + _onPress: function(event: PressEvent) { if (this.props.editable || this.props.editable === undefined) { this.focus(); } }, - _onChange: function(event: Event) { + _onChange: function(event: ChangeEvent) { // Make sure to fire the mostRecentEventCount first so it is already set on // native when the text value is set. - if (this._inputRef) { + if (this._inputRef && this._inputRef.setNativeProps) { this._inputRef.setNativeProps({ mostRecentEventCount: event.nativeEvent.eventCount, }); @@ -1132,7 +1200,7 @@ const TextInput = createReactClass({ this.forceUpdate(); }, - _onSelectionChange: function(event: Event) { + _onSelectionChange: function(event: SelectionChangeEvent) { this.props.onSelectionChange && this.props.onSelectionChange(event); if (!this._inputRef) { @@ -1173,7 +1241,11 @@ const TextInput = createReactClass({ nativeProps.selection = this.props.selection; } - if (Object.keys(nativeProps).length > 0 && this._inputRef) { + if ( + Object.keys(nativeProps).length > 0 && + this._inputRef && + this._inputRef.setNativeProps + ) { this._inputRef.setNativeProps(nativeProps); } @@ -1182,7 +1254,9 @@ const TextInput = createReactClass({ } }, - _onBlur: function(event: Event) { + _onBlur: function(event: BlurEvent) { + // This is a hack to fix https://fburl.com/toehyir8 + // @todo(rsnara) Figure out why this is necessary. this.blur(); if (this.props.onBlur) { this.props.onBlur(event); @@ -1193,11 +1267,11 @@ const TextInput = createReactClass({ } }, - _onTextInput: function(event: Event) { + _onTextInput: function(event: TextInputEvent) { this.props.onTextInput && this.props.onTextInput(event); }, - _onScroll: function(event: Event) { + _onScroll: function(event: ScrollEvent) { this.props.onScroll && this.props.onScroll(event); }, }); diff --git a/Libraries/Components/TextInput/TextInputState.js b/Libraries/Components/TextInput/TextInputState.js index 66682e40a40ade..19acafe2642855 100644 --- a/Libraries/Components/TextInput/TextInputState.js +++ b/Libraries/Components/TextInput/TextInputState.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -10,7 +10,7 @@ * should be funneled through here * * @format - * @flow + * @flow strict-local */ 'use strict'; @@ -42,7 +42,8 @@ function focusTextInput(textFieldID: ?number) { } else if (Platform.OS === 'android') { UIManager.dispatchViewManagerCommand( textFieldID, - UIManager.AndroidTextInput.Commands.focusTextInput, + UIManager.getViewManagerConfig('AndroidTextInput').Commands + .focusTextInput, null, ); } @@ -62,7 +63,8 @@ function blurTextInput(textFieldID: ?number) { } else if (Platform.OS === 'android') { UIManager.dispatchViewManagerCommand( textFieldID, - UIManager.AndroidTextInput.Commands.blurTextInput, + UIManager.getViewManagerConfig('AndroidTextInput').Commands + .blurTextInput, null, ); } diff --git a/Libraries/Components/TextInput/__tests__/TextInput-test.js b/Libraries/Components/TextInput/__tests__/TextInput-test.js new file mode 100644 index 00000000000000..78d6884db03395 --- /dev/null +++ b/Libraries/Components/TextInput/__tests__/TextInput-test.js @@ -0,0 +1,70 @@ +/** + * 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 + * @format + * @flow-strict + */ + +'use strict'; + +const React = require('React'); +const ReactTestRenderer = require('react-test-renderer'); +const TextInput = require('TextInput'); + +import Component from '@reactions/component'; + +const {enter} = require('ReactNativeTestTools'); + +jest.unmock('TextInput'); + +describe('TextInput tests', () => { + let input; + let onChangeListener; + let onChangeTextListener; + const initialValue = 'initialValue'; + beforeEach(() => { + onChangeListener = jest.fn(); + onChangeTextListener = jest.fn(); + const renderTree = ReactTestRenderer.create( + + {({setState, state}) => ( + { + onChangeTextListener(text); + setState({text}); + }} + onChange={event => { + onChangeListener(event); + }} + /> + )} + , + ); + input = renderTree.root.findByType(TextInput); + }); + it('has expected instance functions', () => { + expect(input.instance.isFocused).toBeInstanceOf(Function); // Would have prevented S168585 + expect(input.instance.clear).toBeInstanceOf(Function); + expect(input.instance.focus).toBeInstanceOf(Function); + expect(input.instance.blur).toBeInstanceOf(Function); + expect(input.instance.setNativeProps).toBeInstanceOf(Function); + expect(input.instance.measure).toBeInstanceOf(Function); + expect(input.instance.measureInWindow).toBeInstanceOf(Function); + expect(input.instance.measureLayout).toBeInstanceOf(Function); + }); + it('calls onChange callbacks', () => { + expect(input.props.value).toBe(initialValue); + const message = 'This is a test message'; + enter(input, message); + expect(input.props.value).toBe(message); + expect(onChangeTextListener).toHaveBeenCalledWith(message); + expect(onChangeListener).toHaveBeenCalledWith({ + nativeEvent: {text: message}, + }); + }); +}); diff --git a/Libraries/Components/TimePickerAndroid/TimePickerAndroid.android.js b/Libraries/Components/TimePickerAndroid/TimePickerAndroid.android.js index 139ffa161181b8..87c44621dc089b 100644 --- a/Libraries/Components/TimePickerAndroid/TimePickerAndroid.android.js +++ b/Libraries/Components/TimePickerAndroid/TimePickerAndroid.android.js @@ -1,17 +1,22 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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 + * @flow strict-local */ 'use strict'; const TimePickerModule = require('NativeModules').TimePickerAndroid; +import type { + TimePickerOptions, + TimePickerResult, +} from './TimePickerAndroidTypes'; + /** * Opens the standard Android time picker dialog. * @@ -52,22 +57,18 @@ class TimePickerAndroid { * still be resolved with action being `TimePickerAndroid.dismissedAction` and all the other keys * being undefined. **Always** check whether the `action` before reading the values. */ - static async open(options: Object): Promise { + static async open(options: TimePickerOptions): Promise { return TimePickerModule.open(options); } /** * A time has been selected. */ - static get timeSetAction() { - return 'timeSetAction'; - } + static +timeSetAction: 'timeSetAction' = 'timeSetAction'; /** * The dialog has been dismissed. */ - static get dismissedAction() { - return 'dismissedAction'; - } + static +dismissedAction: 'dismissedAction' = 'dismissedAction'; } module.exports = TimePickerAndroid; diff --git a/Libraries/Components/TimePickerAndroid/TimePickerAndroid.ios.js b/Libraries/Components/TimePickerAndroid/TimePickerAndroid.ios.js index 6a0ecfa656313c..3de6ca4aa06560 100644 --- a/Libraries/Components/TimePickerAndroid/TimePickerAndroid.ios.js +++ b/Libraries/Components/TimePickerAndroid/TimePickerAndroid.ios.js @@ -1,17 +1,22 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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 + * @flow strict-local */ 'use strict'; +import type { + TimePickerOptions, + TimePickerResult, +} from './TimePickerAndroidTypes'; + const TimePickerAndroid = { - async open(options: Object): Promise { + async open(options: TimePickerOptions): Promise { return Promise.reject({ message: 'TimePickerAndroid is not supported on this platform.', }); diff --git a/Libraries/Components/TimePickerAndroid/TimePickerAndroidTypes.js b/Libraries/Components/TimePickerAndroid/TimePickerAndroidTypes.js new file mode 100644 index 00000000000000..aafa572be6c052 --- /dev/null +++ b/Libraries/Components/TimePickerAndroid/TimePickerAndroidTypes.js @@ -0,0 +1,24 @@ +/** + * 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'; + +export type TimePickerOptions = {| + hour?: number, + minute?: number, + is24Hour?: boolean, + mode?: 'clock' | 'spinner' | 'default', +|}; + +export type TimePickerResult = $ReadOnly<{| + action: string, + hour: number, + minute: number, +|}>; diff --git a/Libraries/Components/ToastAndroid/ToastAndroid.android.js b/Libraries/Components/ToastAndroid/ToastAndroid.android.js index a259c17a06ce76..832887453c3fc0 100644 --- a/Libraries/Components/ToastAndroid/ToastAndroid.android.js +++ b/Libraries/Components/ToastAndroid/ToastAndroid.android.js @@ -1,11 +1,11 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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 + * @flow */ 'use strict'; diff --git a/Libraries/Components/ToastAndroid/ToastAndroid.ios.js b/Libraries/Components/ToastAndroid/ToastAndroid.ios.js index adb6032ef6731b..ed5fcf02f40116 100644 --- a/Libraries/Components/ToastAndroid/ToastAndroid.ios.js +++ b/Libraries/Components/ToastAndroid/ToastAndroid.ios.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/Components/ToolbarAndroid/ToolbarAndroid.android.js b/Libraries/Components/ToolbarAndroid/ToolbarAndroid.android.js index 44bcfc03d8046e..e659449be9ac0d 100644 --- a/Libraries/Components/ToolbarAndroid/ToolbarAndroid.android.js +++ b/Libraries/Components/ToolbarAndroid/ToolbarAndroid.android.js @@ -1,32 +1,26 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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 Image = require('Image'); -const NativeMethodsMixin = require('NativeMethodsMixin'); const React = require('React'); -const PropTypes = require('prop-types'); const UIManager = require('UIManager'); -const ViewPropTypes = require('ViewPropTypes'); -const ColorPropType = require('ColorPropType'); -const createReactClass = require('create-react-class'); -const requireNativeComponent = require('requireNativeComponent'); +const ToolbarAndroidNativeComponent = require('ToolbarAndroidNativeComponent'); const resolveAssetSource = require('resolveAssetSource'); -const optionalImageSource = PropTypes.oneOfType([ - Image.propTypes.source, - // Image.propTypes.source is required but we want it to be optional, so we OR - // it with a nullable propType. - PropTypes.oneOf([]), -]); +import type {SyntheticEvent} from 'CoreEventTypes'; +import type {ImageSource} from 'ImageSource'; +import type {ColorValue} from 'StyleSheetTypes'; +import type {ViewProps} from 'ViewPropTypes'; +import type {NativeComponent} from 'ReactNative'; /** * React component that wraps the Android-only [`Toolbar` widget][0]. A Toolbar can display a logo, @@ -63,149 +57,195 @@ const optionalImageSource = PropTypes.oneOfType([ * * [0]: https://developer.android.com/reference/android/support/v7/widget/Toolbar.html */ -const ToolbarAndroid = createReactClass({ - displayName: 'ToolbarAndroid', - mixins: [NativeMethodsMixin], - - propTypes: { - ...ViewPropTypes, - /** - * Sets possible actions on the toolbar as part of the action menu. These are displayed as icons - * or text on the right side of the widget. If they don't fit they are placed in an 'overflow' - * menu. - * - * This property takes an array of objects, where each object has the following keys: - * - * * `title`: **required**, the title of this action - * * `icon`: the icon for this action, e.g. `require('./some_icon.png')` - * * `show`: when to show this action as an icon or hide it in the overflow menu: `always`, - * `ifRoom` or `never` - * * `showWithText`: boolean, whether to show text alongside the icon or not - */ - actions: PropTypes.arrayOf( - PropTypes.shape({ - title: PropTypes.string.isRequired, - icon: optionalImageSource, - show: PropTypes.oneOf(['always', 'ifRoom', 'never']), - showWithText: PropTypes.bool, - }), - ), - /** - * Sets the toolbar logo. - */ - logo: optionalImageSource, - /** - * Sets the navigation icon. - */ - navIcon: optionalImageSource, - /** - * Callback that is called when an action is selected. The only argument that is passed to the - * callback is the position of the action in the actions array. - */ - onActionSelected: PropTypes.func, - /** - * Callback called when the icon is selected. - */ - onIconClicked: PropTypes.func, - /** - * Sets the overflow icon. - */ - overflowIcon: optionalImageSource, - /** - * Sets the toolbar subtitle. - */ - subtitle: PropTypes.string, - /** - * Sets the toolbar subtitle color. - */ - subtitleColor: ColorPropType, - /** - * Sets the toolbar title. - */ - title: PropTypes.string, - /** - * Sets the toolbar title color. - */ - titleColor: ColorPropType, - /** - * Sets the content inset for the toolbar starting edge. - * - * The content inset affects the valid area for Toolbar content other than - * the navigation button and menu. Insets define the minimum margin for - * these components and can be used to effectively align Toolbar content - * along well-known gridlines. - */ - contentInsetStart: PropTypes.number, - /** - * Sets the content inset for the toolbar ending edge. - * - * The content inset affects the valid area for Toolbar content other than - * the navigation button and menu. Insets define the minimum margin for - * these components and can be used to effectively align Toolbar content - * along well-known gridlines. - */ - contentInsetEnd: PropTypes.number, - /** - * Used to set the toolbar direction to RTL. - * In addition to this property you need to add - * - * android:supportsRtl="true" - * - * to your application AndroidManifest.xml and then call - * `setLayoutDirection(LayoutDirection.RTL)` in your MainActivity - * `onCreate` method. - */ - rtl: PropTypes.bool, - /** - * Used to locate this view in end-to-end tests. - */ - testID: PropTypes.string, - }, - render: function() { - const nativeProps = { - ...this.props, +type Action = $ReadOnly<{| + title: string, + icon?: ?ImageSource, + show?: 'always' | 'ifRoom' | 'never', + showWithText?: boolean, +|}>; + +type ToolbarAndroidChangeEvent = SyntheticEvent< + $ReadOnly<{| + position: number, + |}>, +>; + +type ToolbarAndroidProps = $ReadOnly<{| + ...ViewProps, + /** + * or text on the right side of the widget. If they don't fit they are placed in an 'overflow' + * Sets possible actions on the toolbar as part of the action menu. These are displayed as icons + * menu. + * + * This property takes an array of objects, where each object has the following keys: + * + * * `title`: **required**, the title of this action + * * `icon`: the icon for this action, e.g. `require('./some_icon.png')` + * * `show`: when to show this action as an icon or hide it in the overflow menu: `always`, + * `ifRoom` or `never` + * * `showWithText`: boolean, whether to show text alongside the icon or not + */ + actions?: ?Array, + /** + * Sets the toolbar logo. + */ + logo?: ?ImageSource, + /** + * Sets the navigation icon. + */ + navIcon?: ?ImageSource, + /** + * Callback that is called when an action is selected. The only argument that is passed to the + * callback is the position of the action in the actions array. + */ + onActionSelected?: ?(position: number) => void, + /** + * Callback called when the icon is selected. + */ + onIconClicked?: ?() => void, + /** + * Sets the overflow icon. + */ + overflowIcon?: ?ImageSource, + /** + * Sets the toolbar subtitle. + */ + subtitle?: ?string, + /** + * Sets the toolbar subtitle color. + */ + subtitleColor?: ?ColorValue, + /** + * Sets the toolbar title. + */ + title?: ?Stringish, + /** + * Sets the toolbar title color. + */ + titleColor?: ?ColorValue, + /** + * Sets the content inset for the toolbar starting edge. + * + * The content inset affects the valid area for Toolbar content other than + * the navigation button and menu. Insets define the minimum margin for + * these components and can be used to effectively align Toolbar content + * along well-known gridlines. + */ + contentInsetStart?: ?number, + /** + * Sets the content inset for the toolbar ending edge. + * + * The content inset affects the valid area for Toolbar content other than + * the navigation button and menu. Insets define the minimum margin for + * these components and can be used to effectively align Toolbar content + * along well-known gridlines. + */ + contentInsetEnd?: ?number, + /** + * Used to set the toolbar direction to RTL. + * In addition to this property you need to add + * + * android:supportsRtl="true" + * + * to your application AndroidManifest.xml and then call + * `setLayoutDirection(LayoutDirection.RTL)` in your MainActivity + * `onCreate` method. + */ + rtl?: ?boolean, + /** + * Used to locate this view in end-to-end tests. + */ + testID?: ?string, +|}>; + +type Props = $ReadOnly<{| + ...ToolbarAndroidProps, + forwardedRef: ?React.Ref, +|}>; + +class ToolbarAndroid extends React.Component { + _onSelect = (event: ToolbarAndroidChangeEvent) => { + const position = event.nativeEvent.position; + if (position === -1) { + this.props.onIconClicked && this.props.onIconClicked(); + } else { + this.props.onActionSelected && this.props.onActionSelected(position); + } + }; + + render() { + const { + onIconClicked, + onActionSelected, + forwardedRef, + ...otherProps + } = this.props; + + const nativeProps: {...typeof otherProps, nativeActions?: Array} = { + ...otherProps, }; + if (this.props.logo) { nativeProps.logo = resolveAssetSource(this.props.logo); } + if (this.props.navIcon) { nativeProps.navIcon = resolveAssetSource(this.props.navIcon); } + if (this.props.overflowIcon) { nativeProps.overflowIcon = resolveAssetSource(this.props.overflowIcon); } + if (this.props.actions) { const nativeActions = []; for (let i = 0; i < this.props.actions.length; i++) { const action = { - ...this.props.actions[i], + icon: this.props.actions[i].icon, + show: this.props.actions[i].show, }; + if (action.icon) { action.icon = resolveAssetSource(action.icon); } if (action.show) { - action.show = - UIManager.ToolbarAndroid.Constants.ShowAsAction[action.show]; + action.show = UIManager.getViewManagerConfig( + 'ToolbarAndroid', + ).Constants.ShowAsAction[action.show]; } - nativeActions.push(action); + + nativeActions.push({ + ...this.props.actions[i], + ...action, + }); } + nativeProps.nativeActions = nativeActions; } - return ; - }, + return ( + + ); + } +} - _onSelect: function(event) { - const position = event.nativeEvent.position; - if (position === -1) { - this.props.onIconClicked && this.props.onIconClicked(); - } else { - this.props.onActionSelected && this.props.onActionSelected(position); - } +const ToolbarAndroidToExport = React.forwardRef( + ( + props: ToolbarAndroidProps, + forwardedRef: ?React.Ref, + ) => { + return ; }, -}); - -const NativeToolbar = requireNativeComponent('ToolbarAndroid'); +); -module.exports = ToolbarAndroid; +/* $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. */ +module.exports = (ToolbarAndroidToExport: Class< + NativeComponent, +>); diff --git a/Libraries/Components/ToolbarAndroid/ToolbarAndroid.ios.js b/Libraries/Components/ToolbarAndroid/ToolbarAndroid.ios.js index 260d559929796f..7a00b6636cf664 100644 --- a/Libraries/Components/ToolbarAndroid/ToolbarAndroid.ios.js +++ b/Libraries/Components/ToolbarAndroid/ToolbarAndroid.ios.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/Components/ToolbarAndroid/ToolbarAndroidNativeComponent.js b/Libraries/Components/ToolbarAndroid/ToolbarAndroidNativeComponent.js new file mode 100644 index 00000000000000..f58a58083a8255 --- /dev/null +++ b/Libraries/Components/ToolbarAndroid/ToolbarAndroidNativeComponent.js @@ -0,0 +1,133 @@ +/** + * 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 requireNativeComponent = require('requireNativeComponent'); + +import type {SyntheticEvent} from 'CoreEventTypes'; +import type {ImageSource} from 'ImageSource'; +import type {ViewProps} from 'ViewPropTypes'; +import type {NativeComponent} from 'ReactNative'; + +type Action = $ReadOnly<{| + title: string, + icon?: ?ImageSource, + show?: 'always' | 'ifRoom' | 'never', + showWithText?: boolean, +|}>; + +type ToolbarAndroidChangeEvent = SyntheticEvent< + $ReadOnly<{| + position: number, + |}>, +>; + +type NativeProps = $ReadOnly<{| + onSelect: (event: ToolbarAndroidChangeEvent) => mixed, + nativeActions?: Array, +|}>; + +type ColorValue = null | string; + +type ToolbarAndroidProps = $ReadOnly<{| + ...ViewProps, + ...NativeProps, + /** + * or text on the right side of the widget. If they don't fit they are placed in an 'overflow' + * Sets possible actions on the toolbar as part of the action menu. These are displayed as icons + * menu. + * + * This property takes an array of objects, where each object has the following keys: + * + * * `title`: **required**, the title of this action + * * `icon`: the icon for this action, e.g. `require('./some_icon.png')` + * * `show`: when to show this action as an icon or hide it in the overflow menu: `always`, + * `ifRoom` or `never` + * * `showWithText`: boolean, whether to show text alongside the icon or not + */ + actions?: ?Array, + /** + * Sets the toolbar logo. + */ + logo?: ?ImageSource, + /** + * Sets the navigation icon. + */ + navIcon?: ?ImageSource, + /** + * Callback that is called when an action is selected. The only argument that is passed to the + * callback is the position of the action in the actions array. + */ + onActionSelected?: ?(position: number) => void, + /** + * Callback called when the icon is selected. + */ + onIconClicked?: ?() => void, + /** + * Sets the overflow icon. + */ + overflowIcon?: ?ImageSource, + /** + * Sets the toolbar subtitle. + */ + subtitle?: ?string, + /** + * Sets the toolbar subtitle color. + */ + subtitleColor?: ?ColorValue, + /** + * Sets the toolbar title. + */ + title?: ?Stringish, + /** + * Sets the toolbar title color. + */ + titleColor?: ?ColorValue, + /** + * Sets the content inset for the toolbar starting edge. + * + * The content inset affects the valid area for Toolbar content other than + * the navigation button and menu. Insets define the minimum margin for + * these components and can be used to effectively align Toolbar content + * along well-known gridlines. + */ + contentInsetStart?: ?number, + /** + * Sets the content inset for the toolbar ending edge. + * + * The content inset affects the valid area for Toolbar content other than + * the navigation button and menu. Insets define the minimum margin for + * these components and can be used to effectively align Toolbar content + * along well-known gridlines. + */ + contentInsetEnd?: ?number, + /** + * Used to set the toolbar direction to RTL. + * In addition to this property you need to add + * + * android:supportsRtl="true" + * + * to your application AndroidManifest.xml and then call + * `setLayoutDirection(LayoutDirection.RTL)` in your MainActivity + * `onCreate` method. + */ + rtl?: ?boolean, + /** + * Used to locate this view in end-to-end tests. + */ + testID?: ?string, +|}>; + +type NativeToolbarAndroidProps = Class>; + +module.exports = ((requireNativeComponent( + 'ToolbarAndroid', +): any): NativeToolbarAndroidProps); diff --git a/Libraries/Components/Touchable/BoundingDimensions.js b/Libraries/Components/Touchable/BoundingDimensions.js index 755da5ab8937aa..28622a5b2da7f1 100644 --- a/Libraries/Components/Touchable/BoundingDimensions.js +++ b/Libraries/Components/Touchable/BoundingDimensions.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/Components/Touchable/PooledClass.js b/Libraries/Components/Touchable/PooledClass.js index a6934facae7049..f6fc1ee852fce6 100644 --- a/Libraries/Components/Touchable/PooledClass.js +++ b/Libraries/Components/Touchable/PooledClass.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2013-present, Facebook, Inc. + * 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. @@ -10,7 +10,7 @@ 'use strict'; -const invariant = require('fbjs/lib/invariant'); +const invariant = require('invariant'); /** * Static poolers. Several custom versions for each potential number of diff --git a/Libraries/Components/Touchable/Position.js b/Libraries/Components/Touchable/Position.js index 19899d74c49820..bc6a45cc6c6a1f 100644 --- a/Libraries/Components/Touchable/Position.js +++ b/Libraries/Components/Touchable/Position.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/Components/Touchable/Touchable.js b/Libraries/Components/Touchable/Touchable.js index 3f1874c8aac4a4..65a82b1c4fb113 100644 --- a/Libraries/Components/Touchable/Touchable.js +++ b/Libraries/Components/Touchable/Touchable.js @@ -1,9 +1,10 @@ /** - * Copyright (c) 2013-present, Facebook, Inc. + * 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 * @format */ @@ -14,14 +15,30 @@ const Platform = require('Platform'); const Position = require('Position'); const React = require('React'); const ReactNative = require('ReactNative'); +const StyleSheet = require('StyleSheet'); const TVEventHandler = require('TVEventHandler'); -const TouchEventUtils = require('fbjs/lib/TouchEventUtils'); const UIManager = require('UIManager'); const View = require('View'); const keyMirror = require('fbjs/lib/keyMirror'); const normalizeColor = require('normalizeColor'); +import type {PressEvent} from 'CoreEventTypes'; +import type {EdgeInsetsProp} from 'EdgeInsetsPropType'; + +const extractSingleTouch = nativeEvent => { + const touches = nativeEvent.touches; + const changedTouches = nativeEvent.changedTouches; + const hasTouches = touches && touches.length > 0; + const hasChangedTouches = changedTouches && changedTouches.length > 0; + + return !hasTouches && hasChangedTouches + ? changedTouches[0] + : hasTouches + ? touches[0] + : nativeEvent; +}; + /** * `Touchable`: Taps done right. * @@ -110,6 +127,7 @@ const normalizeColor = require('normalizeColor'); /** * Touchable states. */ + const States = keyMirror({ NOT_RESPONDER: null, // Not the responder RESPONDER_INACTIVE_PRESS_IN: null, // Responder, inactive, in the `PressRect` @@ -121,10 +139,33 @@ const States = keyMirror({ ERROR: null, }); -/** +type State = + | typeof States.NOT_RESPONDER + | typeof States.RESPONDER_INACTIVE_PRESS_IN + | typeof States.RESPONDER_INACTIVE_PRESS_OUT + | typeof States.RESPONDER_ACTIVE_PRESS_IN + | typeof States.RESPONDER_ACTIVE_PRESS_OUT + | typeof States.RESPONDER_ACTIVE_LONG_PRESS_IN + | typeof States.RESPONDER_ACTIVE_LONG_PRESS_OUT + | typeof States.ERROR; + +/* * Quick lookup map for states that are considered to be "active" */ + +const baseStatesConditions = { + NOT_RESPONDER: false, + RESPONDER_INACTIVE_PRESS_IN: false, + RESPONDER_INACTIVE_PRESS_OUT: false, + RESPONDER_ACTIVE_PRESS_IN: false, + RESPONDER_ACTIVE_PRESS_OUT: false, + RESPONDER_ACTIVE_LONG_PRESS_IN: false, + RESPONDER_ACTIVE_LONG_PRESS_OUT: false, + ERROR: false, +}; + const IsActive = { + ...baseStatesConditions, RESPONDER_ACTIVE_PRESS_OUT: true, RESPONDER_ACTIVE_PRESS_IN: true, }; @@ -134,12 +175,14 @@ const IsActive = { * therefore eligible to result in a "selection" if the press stops. */ const IsPressingIn = { + ...baseStatesConditions, RESPONDER_INACTIVE_PRESS_IN: true, RESPONDER_ACTIVE_PRESS_IN: true, RESPONDER_ACTIVE_LONG_PRESS_IN: true, }; const IsLongPressingIn = { + ...baseStatesConditions, RESPONDER_ACTIVE_LONG_PRESS_IN: true, }; @@ -156,6 +199,15 @@ const Signals = keyMirror({ LONG_PRESS_DETECTED: null, }); +type Signal = + | typeof Signals.DELAY + | typeof Signals.RESPONDER_GRANT + | typeof Signals.RESPONDER_RELEASE + | typeof Signals.RESPONDER_TERMINATED + | typeof Signals.ENTER_PRESS_RECT + | typeof Signals.LEAVE_PRESS_RECT + | typeof Signals.LONG_PRESS_DETECTED; + /** * Mapping from States x Signals => States */ @@ -390,7 +442,7 @@ const TouchableMixin = { * @param {SyntheticEvent} e Synthetic event from event system. * */ - touchableHandleResponderGrant: function(e) { + touchableHandleResponderGrant: function(e: PressEvent) { const dispatchID = e.currentTarget; // Since e is used in a callback invoked on another event loop // (as in setTimeout etc), we need to call e.persist() on the @@ -431,29 +483,21 @@ const TouchableMixin = { /** * Place as callback for a DOM element's `onResponderRelease` event. */ - touchableHandleResponderRelease: function(e) { + touchableHandleResponderRelease: function(e: PressEvent) { this._receiveSignal(Signals.RESPONDER_RELEASE, e); }, /** * Place as callback for a DOM element's `onResponderTerminate` event. */ - touchableHandleResponderTerminate: function(e) { + touchableHandleResponderTerminate: function(e: PressEvent) { this._receiveSignal(Signals.RESPONDER_TERMINATED, e); }, /** * Place as callback for a DOM element's `onResponderMove` event. */ - touchableHandleResponderMove: function(e) { - // Not enough time elapsed yet, wait for highlight - - // this is just a perf optimization. - if ( - this.state.touchable.touchState === States.RESPONDER_INACTIVE_PRESS_IN - ) { - return; - } - + touchableHandleResponderMove: function(e: PressEvent) { // Measurement may not have returned yet. if (!this.state.touchable.positionOnActivate) { return; @@ -480,13 +524,13 @@ const TouchableMixin = { : null; if (hitSlop) { - pressExpandLeft += hitSlop.left; - pressExpandTop += hitSlop.top; - pressExpandRight += hitSlop.right; - pressExpandBottom += hitSlop.bottom; + pressExpandLeft += hitSlop.left || 0; + pressExpandTop += hitSlop.top || 0; + pressExpandRight += hitSlop.right || 0; + pressExpandBottom += hitSlop.bottom || 0; } - const touch = TouchEventUtils.extractSingleTouch(e.nativeEvent); + const touch = extractSingleTouch(e.nativeEvent); const pageX = touch && touch.pageX; const pageY = touch && touch.pageY; @@ -531,7 +575,8 @@ const TouchableMixin = { * visually distinguish the `VisualRect` so that the user knows that it * currently has the focus. Most platforms only support a single element being * focused at a time, in which case there may have been a previously focused - * element that was blurred just prior to this. + * element that was blurred just prior to this. This can be overridden when + * using `Touchable.Mixin.withoutDefaultFocusAndBlur`. */ touchableHandleFocus: function(e: Event) { this.props.onFocus && this.props.onFocus(e); @@ -542,6 +587,8 @@ const TouchableMixin = { * visually distinguish the `VisualRect` so that the user knows that it * no longer has focus. Most platforms only support a single element being * focused at a time, in which case the focus may have moved to another. + * This can be overridden when using + * `Touchable.Mixin.withoutDefaultFocusAndBlur`. */ touchableHandleBlur: function(e: Event) { this.props.onBlur && this.props.onBlur(e); @@ -632,7 +679,14 @@ const TouchableMixin = { UIManager.measure(tag, this._handleQueryLayout); }, - _handleQueryLayout: function(l, t, w, h, globalX, globalY) { + _handleQueryLayout: function( + l: number, + t: number, + w: number, + h: number, + globalX: number, + globalY: number, + ) { //don't do anything UIManager failed to measure node if (!l && !t && !w && !h && !globalX && !globalY) { return; @@ -651,12 +705,12 @@ const TouchableMixin = { ); }, - _handleDelay: function(e) { + _handleDelay: function(e: PressEvent) { this.touchableDelayTimeout = null; this._receiveSignal(Signals.DELAY, e); }, - _handleLongDelay: function(e) { + _handleLongDelay: function(e: PressEvent) { this.longPressDelayTimeout = null; const curState = this.state.touchable.touchState; if ( @@ -684,7 +738,7 @@ const TouchableMixin = { * @throws Error if invalid state transition or unrecognized signal. * @sideeffects */ - _receiveSignal: function(signal, e) { + _receiveSignal: function(signal: Signal, e: PressEvent) { const responderID = this.state.touchable.responderID; const curState = this.state.touchable.touchState; const nextState = Transitions[curState] && Transitions[curState][signal]; @@ -724,15 +778,15 @@ const TouchableMixin = { this.longPressDelayTimeout = null; }, - _isHighlight: function(state) { + _isHighlight: function(state: State) { return ( state === States.RESPONDER_ACTIVE_PRESS_IN || state === States.RESPONDER_ACTIVE_LONG_PRESS_IN ); }, - _savePressInLocation: function(e) { - const touch = TouchEventUtils.extractSingleTouch(e.nativeEvent); + _savePressInLocation: function(e: PressEvent) { + const touch = extractSingleTouch(e.nativeEvent); const pageX = touch && touch.pageX; const pageY = touch && touch.pageY; const locationX = touch && touch.locationX; @@ -740,7 +794,12 @@ const TouchableMixin = { this.pressInLocation = {pageX, pageY, locationX, locationY}; }, - _getDistanceBetweenPoints: function(aX, aY, bX, bY) { + _getDistanceBetweenPoints: function( + aX: number, + aY: number, + bX: number, + bY: number, + ) { const deltaX = aX - bX; const deltaY = aY - bY; return Math.sqrt(deltaX * deltaX + deltaY * deltaY); @@ -757,7 +816,12 @@ const TouchableMixin = { * @param {Event} e Native event. * @sideeffects */ - _performSideEffectsForTransition: function(curState, nextState, signal, e) { + _performSideEffectsForTransition: function( + curState: State, + nextState: State, + signal: Signal, + e: PressEvent, + ) { const curIsHighlight = this._isHighlight(curState); const newIsHighlight = this._isHighlight(nextState); @@ -769,7 +833,12 @@ const TouchableMixin = { this._cancelLongPressDelayTimeout(); } - if (!IsActive[curState] && IsActive[nextState]) { + const isInitialTransition = + curState === States.NOT_RESPONDER && + nextState === States.RESPONDER_INACTIVE_PRESS_IN; + + const isActiveTransition = !IsActive[curState] && IsActive[nextState]; + if (isInitialTransition || isActiveTransition) { this._remeasureMetricsOnActivation(); } @@ -812,12 +881,12 @@ const TouchableMixin = { UIManager.playTouchSound(); }, - _startHighlight: function(e) { + _startHighlight: function(e: PressEvent) { this._savePressInLocation(e); this.touchableHandleActivePressIn && this.touchableHandleActivePressIn(e); }, - _endHighlight: function(e) { + _endHighlight: function(e: PressEvent) { if (this.touchableHandleActivePressOut) { if ( this.touchableGetPressOutDelayMS && @@ -831,15 +900,36 @@ const TouchableMixin = { } } }, + + withoutDefaultFocusAndBlur: {}, }; +/** + * Provide an optional version of the mixin where `touchableHandleFocus` and + * `touchableHandleBlur` can be overridden. This allows appropriate defaults to + * be set on TV platforms, without breaking existing implementations of + * `Touchable`. + */ +const { + touchableHandleFocus, + touchableHandleBlur, + ...TouchableMixinWithoutDefaultFocusAndBlur +} = TouchableMixin; +TouchableMixin.withoutDefaultFocusAndBlur = TouchableMixinWithoutDefaultFocusAndBlur; + const Touchable = { Mixin: TouchableMixin, TOUCH_TARGET_DEBUG: false, // Highlights all touchable targets. Toggle with Inspector. /** * Renders a debugging overlay to visualize touch target with hitSlop (might not work on Android). */ - renderDebugView: ({color, hitSlop}) => { + renderDebugView: ({ + color, + hitSlop, + }: { + color: string | number, + hitSlop: EdgeInsetsProp, + }) => { if (!Touchable.TOUCH_TARGET_DEBUG) { return null; } @@ -853,22 +943,34 @@ const Touchable = { for (const key in hitSlop) { debugHitSlopStyle[key] = -hitSlop[key]; } + const normalizedColor = normalizeColor(color); + if (typeof normalizedColor !== 'number') { + return null; + } const hexColor = - '#' + ('00000000' + normalizeColor(color).toString(16)).substr(-8); + '#' + ('00000000' + normalizedColor.toString(16)).substr(-8); return ( ); }, }; +const styles = StyleSheet.create({ + debug: { + position: 'absolute', + borderWidth: 1, + borderStyle: 'dashed', + }, +}); + module.exports = Touchable; diff --git a/Libraries/Components/Touchable/TouchableBounce.js b/Libraries/Components/Touchable/TouchableBounce.js index ce47db20c3d25c..ff1b2d1d560d25 100644 --- a/Libraries/Components/Touchable/TouchableBounce.js +++ b/Libraries/Components/Touchable/TouchableBounce.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -10,20 +10,21 @@ 'use strict'; const Animated = require('Animated'); -const EdgeInsetsPropType = require('EdgeInsetsPropType'); +const DeprecatedViewPropTypes = require('DeprecatedViewPropTypes'); +const DeprecatedEdgeInsetsPropType = require('DeprecatedEdgeInsetsPropType'); const NativeMethodsMixin = require('NativeMethodsMixin'); -const React = require('React'); -const createReactClass = require('create-react-class'); +const Platform = require('Platform'); const PropTypes = require('prop-types'); +const React = require('React'); const Touchable = require('Touchable'); const TouchableWithoutFeedback = require('TouchableWithoutFeedback'); -const ViewPropTypes = require('ViewPropTypes'); + +const createReactClass = require('create-react-class'); import type {EdgeInsetsProp} from 'EdgeInsetsPropType'; -import type {Props as TouchableWithoutFeedbackProps} from 'TouchableWithoutFeedback'; import type {ViewStyleProp} from 'StyleSheet'; - -type Event = Object; +import type {Props as TouchableWithoutFeedbackProps} from 'TouchableWithoutFeedback'; +import type {PressEvent} from 'CoreEventTypes'; type State = { animationID: ?number, @@ -35,8 +36,8 @@ const PRESS_RETENTION_OFFSET = {top: 20, left: 20, right: 20, bottom: 30}; type Props = $ReadOnly<{| ...TouchableWithoutFeedbackProps, - onPressWithCompletion?: ?Function, - onPressAnimationComplete?: ?Function, + onPressWithCompletion?: ?(fn: () => void) => void, + onPressAnimationComplete?: ?() => void, pressRetentionOffset?: ?EdgeInsetsProp, releaseVelocity?: ?number, releaseBounciness?: ?number, @@ -52,9 +53,12 @@ type Props = $ReadOnly<{| */ const TouchableBounce = ((createReactClass({ displayName: 'TouchableBounce', - mixins: [Touchable.Mixin, NativeMethodsMixin], + mixins: [Touchable.Mixin.withoutDefaultFocusAndBlur, NativeMethodsMixin], propTypes: { + /* $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. */ ...TouchableWithoutFeedback.propTypes, // The function passed takes a callback to start the animation which should // be run after this onPress handler is done. You can use this (for example) @@ -69,14 +73,14 @@ const TouchableBounce = ((createReactClass({ * reactivated! Move it back and forth several times while the scroll view * is disabled. Ensure you pass in a constant to reduce memory allocations. */ - pressRetentionOffset: EdgeInsetsPropType, + pressRetentionOffset: DeprecatedEdgeInsetsPropType, releaseVelocity: PropTypes.number.isRequired, releaseBounciness: PropTypes.number.isRequired, /** * Style to apply to the container/underlay. Most commonly used to make sure * rounded corners match the wrapped component. */ - style: ViewPropTypes.style, + style: DeprecatedViewPropTypes.style, }, getDefaultProps: function() { @@ -85,6 +89,9 @@ const TouchableBounce = ((createReactClass({ getInitialState: function(): State { return { + /* $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. */ ...this.touchableGetInitialState(), scale: new Animated.Value(1), }; @@ -94,7 +101,7 @@ const TouchableBounce = ((createReactClass({ value: number, velocity: number, bounciness: number, - callback?: ?Function, + callback?: ?() => void, ) { Animated.spring(this.state.scale, { toValue: value, @@ -108,17 +115,31 @@ const TouchableBounce = ((createReactClass({ * `Touchable.Mixin` self callbacks. The mixin will invoke these if they are * defined on your component. */ - touchableHandleActivePressIn: function(e: Event) { + touchableHandleActivePressIn: function(e: PressEvent) { this.bounceTo(0.93, 0.1, 0); this.props.onPressIn && this.props.onPressIn(e); }, - touchableHandleActivePressOut: function(e: Event) { + touchableHandleActivePressOut: function(e: PressEvent) { this.bounceTo(1, 0.4, 0); this.props.onPressOut && this.props.onPressOut(e); }, - touchableHandlePress: function(e: Event) { + touchableHandleFocus: function(e: Event) { + if (Platform.isTV) { + this.bounceTo(0.93, 0.1, 0); + } + this.props.onFocus && this.props.onFocus(e); + }, + + touchableHandleBlur: function(e: Event) { + if (Platform.isTV) { + this.bounceTo(1, 0.4, 0); + } + this.props.onBlur && this.props.onBlur(e); + }, + + touchableHandlePress: function(e: PressEvent) { const onPressWithCompletion = this.props.onPressWithCompletion; if (onPressWithCompletion) { onPressWithCompletion(() => { @@ -146,7 +167,7 @@ const TouchableBounce = ((createReactClass({ return this.props.pressRetentionOffset || PRESS_RETENTION_OFFSET; }, - touchableGetHitSlop: function(): ?Object { + touchableGetHitSlop: function(): ?EdgeInsetsProp { return this.props.hitSlop; }, @@ -166,13 +187,31 @@ const TouchableBounce = ((createReactClass({ nativeID={this.props.nativeID} testID={this.props.testID} hitSlop={this.props.hitSlop} + /* $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. */ onStartShouldSetResponder={this.touchableHandleStartShouldSetResponder} onResponderTerminationRequest={ + /* $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. */ this.touchableHandleResponderTerminationRequest } + /* $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. */ onResponderGrant={this.touchableHandleResponderGrant} + /* $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. */ onResponderMove={this.touchableHandleResponderMove} + /* $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. */ onResponderRelease={this.touchableHandleResponderRelease} + /* $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. */ onResponderTerminate={this.touchableHandleResponderTerminate}> {this.props.children} {Touchable.renderDebugView({ diff --git a/Libraries/Components/Touchable/TouchableHighlight.js b/Libraries/Components/Touchable/TouchableHighlight.js index fd178341a4e239..4d1696cfcd5493 100644 --- a/Libraries/Components/Touchable/TouchableHighlight.js +++ b/Libraries/Components/Touchable/TouchableHighlight.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -9,25 +9,26 @@ */ 'use strict'; -const ColorPropType = require('ColorPropType'); +const DeprecatedColorPropType = require('DeprecatedColorPropType'); +const DeprecatedViewPropTypes = require('DeprecatedViewPropTypes'); const NativeMethodsMixin = require('NativeMethodsMixin'); -const PropTypes = require('prop-types'); const Platform = require('Platform'); +const PropTypes = require('prop-types'); const React = require('React'); const ReactNativeViewAttributes = require('ReactNativeViewAttributes'); const StyleSheet = require('StyleSheet'); const Touchable = require('Touchable'); const TouchableWithoutFeedback = require('TouchableWithoutFeedback'); const View = require('View'); -const ViewPropTypes = require('ViewPropTypes'); const createReactClass = require('create-react-class'); const ensurePositiveDelayProps = require('ensurePositiveDelayProps'); import type {PressEvent} from 'CoreEventTypes'; -import type {Props as TouchableWithoutFeedbackProps} from 'TouchableWithoutFeedback'; import type {ViewStyleProp} from 'StyleSheet'; import type {ColorValue} from 'StyleSheetTypes'; +import type {Props as TouchableWithoutFeedbackProps} from 'TouchableWithoutFeedback'; +import type {TVParallaxPropertiesType} from 'TVViewPropTypes'; const DEFAULT_PROPS = { activeOpacity: 0.85, @@ -39,7 +40,7 @@ const PRESS_RETENTION_OFFSET = {top: 20, left: 20, right: 20, bottom: 30}; type IOSProps = $ReadOnly<{| hasTVPreferredFocus?: ?boolean, - tvParallaxProperties?: ?Object, + tvParallaxProperties?: ?TVParallaxPropertiesType, |}>; type Props = $ReadOnly<{| @@ -49,8 +50,8 @@ type Props = $ReadOnly<{| activeOpacity?: ?number, underlayColor?: ?ColorValue, style?: ?ViewStyleProp, - onShowUnderlay?: ?Function, - onHideUnderlay?: ?Function, + onShowUnderlay?: ?() => void, + onHideUnderlay?: ?() => void, testOnly_pressed?: ?boolean, |}>; @@ -154,6 +155,9 @@ type Props = $ReadOnly<{| const TouchableHighlight = ((createReactClass({ displayName: 'TouchableHighlight', propTypes: { + /* $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. */ ...TouchableWithoutFeedback.propTypes, /** * Determines what the opacity of the wrapped view should be when touch is @@ -164,12 +168,12 @@ const TouchableHighlight = ((createReactClass({ * The color of the underlay that will show through when the touch is * active. */ - underlayColor: ColorPropType, + underlayColor: DeprecatedColorPropType, /** * Style to apply to the container/underlay. Most commonly used to make sure * rounded corners match the wrapped component. */ - style: ViewPropTypes.style, + style: DeprecatedViewPropTypes.style, /** * Called immediately after the underlay is shown */ @@ -185,18 +189,7 @@ const TouchableHighlight = ((createReactClass({ */ hasTVPreferredFocus: PropTypes.bool, /** - * *(Apple TV only)* Object with properties to control Apple TV parallax effects. - * - * enabled: If true, parallax effects are enabled. Defaults to true. - * shiftDistanceX: Defaults to 2.0. - * shiftDistanceY: Defaults to 2.0. - * tiltAngle: Defaults to 0.05. - * magnification: Defaults to 1.0. - * pressMagnification: Defaults to 1.0. - * pressDuration: Defaults to 0.3. - * pressDelay: Defaults to 0.0. - * - * @platform ios + * Apple TV parallax effects */ tvParallaxProperties: PropTypes.object, /** @@ -205,14 +198,20 @@ const TouchableHighlight = ((createReactClass({ testOnly_pressed: PropTypes.bool, }, - mixins: [NativeMethodsMixin, Touchable.Mixin], + mixins: [NativeMethodsMixin, Touchable.Mixin.withoutDefaultFocusAndBlur], getDefaultProps: () => DEFAULT_PROPS, getInitialState: function() { + /* $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. */ this._isMounted = false; if (this.props.testOnly_pressed) { return { + /* $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. */ ...this.touchableGetInitialState(), extraChildStyle: { opacity: this.props.activeOpacity, @@ -223,6 +222,9 @@ const TouchableHighlight = ((createReactClass({ }; } else { return { + /* $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. */ ...this.touchableGetInitialState(), extraChildStyle: null, extraUnderlayStyle: null, @@ -231,12 +233,21 @@ const TouchableHighlight = ((createReactClass({ }, componentDidMount: function() { + /* $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. */ this._isMounted = true; ensurePositiveDelayProps(this.props); }, componentWillUnmount: function() { + /* $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. */ this._isMounted = false; + /* $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. */ clearTimeout(this._hideTimeout); }, @@ -254,23 +265,52 @@ const TouchableHighlight = ((createReactClass({ * defined on your component. */ touchableHandleActivePressIn: function(e: PressEvent) { + /* $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. */ clearTimeout(this._hideTimeout); + /* $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. */ this._hideTimeout = null; this._showUnderlay(); this.props.onPressIn && this.props.onPressIn(e); }, touchableHandleActivePressOut: function(e: PressEvent) { + /* $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. */ if (!this._hideTimeout) { this._hideUnderlay(); } this.props.onPressOut && this.props.onPressOut(e); }, + touchableHandleFocus: function(e: Event) { + if (Platform.isTV) { + this._showUnderlay(); + } + this.props.onFocus && this.props.onFocus(e); + }, + + touchableHandleBlur: function(e: Event) { + if (Platform.isTV) { + this._hideUnderlay(); + } + this.props.onBlur && this.props.onBlur(e); + }, + touchableHandlePress: function(e: PressEvent) { + /* $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. */ clearTimeout(this._hideTimeout); if (!Platform.isTV) { this._showUnderlay(); + /* $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. */ this._hideTimeout = setTimeout( this._hideUnderlay, this.props.delayPressOut, @@ -304,6 +344,9 @@ const TouchableHighlight = ((createReactClass({ }, _showUnderlay: function() { + /* $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. */ if (!this._isMounted || !this._hasPressHandler()) { return; } @@ -319,7 +362,13 @@ const TouchableHighlight = ((createReactClass({ }, _hideUnderlay: function() { + /* $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. */ clearTimeout(this._hideTimeout); + /* $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. */ this._hideTimeout = null; if (this.props.testOnly_pressed) { return; @@ -360,13 +409,31 @@ const TouchableHighlight = ((createReactClass({ isTVSelectable={true} tvParallaxProperties={this.props.tvParallaxProperties} hasTVPreferredFocus={this.props.hasTVPreferredFocus} + /* $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. */ onStartShouldSetResponder={this.touchableHandleStartShouldSetResponder} onResponderTerminationRequest={ + /* $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. */ this.touchableHandleResponderTerminationRequest } + /* $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. */ onResponderGrant={this.touchableHandleResponderGrant} + /* $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. */ onResponderMove={this.touchableHandleResponderMove} + /* $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. */ onResponderRelease={this.touchableHandleResponderRelease} + /* $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. */ onResponderTerminate={this.touchableHandleResponderTerminate} nativeID={this.props.nativeID} testID={this.props.testID}> diff --git a/Libraries/Components/Touchable/TouchableNativeFeedback.android.js b/Libraries/Components/Touchable/TouchableNativeFeedback.android.js index db99fbde111bad..16beec536b41d6 100644 --- a/Libraries/Components/Touchable/TouchableNativeFeedback.android.js +++ b/Libraries/Components/Touchable/TouchableNativeFeedback.android.js @@ -1,9 +1,10 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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 */ @@ -16,11 +17,14 @@ const ReactNative = require('ReactNative'); const Touchable = require('Touchable'); const TouchableWithoutFeedback = require('TouchableWithoutFeedback'); const UIManager = require('UIManager'); +const View = require('View'); const createReactClass = require('create-react-class'); const ensurePositiveDelayProps = require('ensurePositiveDelayProps'); const processColor = require('processColor'); +import type {PressEvent} from 'CoreEventTypes'; + const rippleBackgroundPropType = PropTypes.shape({ type: PropTypes.oneOf(['RippleAndroid']), color: PropTypes.number, @@ -37,8 +41,6 @@ const backgroundPropType = PropTypes.oneOfType([ themeAttributeBackgroundPropType, ]); -type Event = Object; - const PRESS_RETENTION_OFFSET = {top: 20, left: 20, right: 20, bottom: 30}; /** @@ -73,6 +75,9 @@ const PRESS_RETENTION_OFFSET = {top: 20, left: 20, right: 20, bottom: 30}; const TouchableNativeFeedback = createReactClass({ displayName: 'TouchableNativeFeedback', propTypes: { + /* $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. */ ...TouchableWithoutFeedback.propTypes, /** @@ -105,7 +110,10 @@ const TouchableNativeFeedback = createReactClass({ * Creates an object that represents android theme's default background for * selectable elements (?android:attr/selectableItemBackground). */ - SelectableBackground: function() { + SelectableBackground: function(): { + type: 'ThemeAttrAndroid', + attribute: 'selectableItemBackground', + } { return {type: 'ThemeAttrAndroid', attribute: 'selectableItemBackground'}; }, /** @@ -113,7 +121,10 @@ const TouchableNativeFeedback = createReactClass({ * selectable elements (?android:attr/selectableItemBackgroundBorderless). * Available on android API level 21+. */ - SelectableBackgroundBorderless: function() { + SelectableBackgroundBorderless: function(): { + type: 'ThemeAttrAndroid', + attribute: 'selectableItemBackgroundBorderless', + } { return { type: 'ThemeAttrAndroid', attribute: 'selectableItemBackgroundBorderless', @@ -129,7 +140,14 @@ const TouchableNativeFeedback = createReactClass({ * @param color The ripple color * @param borderless If the ripple can render outside it's bounds */ - Ripple: function(color: string, borderless: boolean) { + Ripple: function( + color: string, + borderless: boolean, + ): { + type: 'RippleAndroid', + color: ?number, + borderless: boolean, + } { return { type: 'RippleAndroid', color: processColor(color), @@ -137,7 +155,7 @@ const TouchableNativeFeedback = createReactClass({ }; }, - canUseNativeForeground: function() { + canUseNativeForeground: function(): boolean { return Platform.OS === 'android' && Platform.Version >= 23; }, }, @@ -166,27 +184,36 @@ const TouchableNativeFeedback = createReactClass({ * `Touchable.Mixin` self callbacks. The mixin will invoke these if they are * defined on your component. */ - touchableHandleActivePressIn: function(e: Event) { + touchableHandleActivePressIn: function(e: PressEvent) { this.props.onPressIn && this.props.onPressIn(e); this._dispatchPressedStateChange(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. */ if (this.pressInLocation) { this._dispatchHotspotUpdate( + /* $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. */ this.pressInLocation.locationX, + /* $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. */ this.pressInLocation.locationY, ); } }, - touchableHandleActivePressOut: function(e: Event) { + touchableHandleActivePressOut: function(e: PressEvent) { this.props.onPressOut && this.props.onPressOut(e); this._dispatchPressedStateChange(false); }, - touchableHandlePress: function(e: Event) { + touchableHandlePress: function(e: PressEvent) { this.props.onPress && this.props.onPress(e); }, - touchableHandleLongPress: function(e: Event) { + touchableHandleLongPress: function(e: PressEvent) { this.props.onLongPress && this.props.onLongPress(e); }, @@ -222,7 +249,7 @@ const TouchableNativeFeedback = createReactClass({ _dispatchHotspotUpdate: function(destX, destY) { UIManager.dispatchViewManagerCommand( ReactNative.findNodeHandle(this), - UIManager.RCTView.Commands.hotspotUpdate, + UIManager.getViewManagerConfig('RCTView').Commands.hotspotUpdate, [destX || 0, destY || 0], ); }, @@ -230,7 +257,7 @@ const TouchableNativeFeedback = createReactClass({ _dispatchPressedStateChange: function(pressed) { UIManager.dispatchViewManagerCommand( ReactNative.findNodeHandle(this), - UIManager.RCTView.Commands.setPressed, + UIManager.getViewManagerConfig('RCTView').Commands.setPressed, [pressed], ); }, @@ -238,7 +265,7 @@ const TouchableNativeFeedback = createReactClass({ render: function() { const child = React.Children.only(this.props.children); let children = child.props.children; - if (Touchable.TOUCH_TARGET_DEBUG && child.type.displayName === 'View') { + if (Touchable.TOUCH_TARGET_DEBUG && child.type === View) { if (!Array.isArray(children)) { children = [children]; } diff --git a/Libraries/Components/Touchable/TouchableNativeFeedback.ios.js b/Libraries/Components/Touchable/TouchableNativeFeedback.ios.js index b0770047bbeef8..e32f30deda1124 100644 --- a/Libraries/Components/Touchable/TouchableNativeFeedback.ios.js +++ b/Libraries/Components/Touchable/TouchableNativeFeedback.ios.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/Components/Touchable/TouchableOpacity.js b/Libraries/Components/Touchable/TouchableOpacity.js index e20b934ae18369..0451f8dfff1e7f 100644 --- a/Libraries/Components/Touchable/TouchableOpacity.js +++ b/Libraries/Components/Touchable/TouchableOpacity.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -13,9 +13,9 @@ const Animated = require('Animated'); const Easing = require('Easing'); const NativeMethodsMixin = require('NativeMethodsMixin'); +const Platform = require('Platform'); const React = require('React'); const PropTypes = require('prop-types'); -const TimerMixin = require('react-timer-mixin'); const Touchable = require('Touchable'); const TouchableWithoutFeedback = require('TouchableWithoutFeedback'); @@ -25,14 +25,14 @@ const flattenStyle = require('flattenStyle'); import type {Props as TouchableWithoutFeedbackProps} from 'TouchableWithoutFeedback'; import type {ViewStyleProp} from 'StyleSheet'; - -type Event = Object; +import type {TVParallaxPropertiesType} from 'TVViewPropTypes'; +import type {PressEvent} from 'CoreEventTypes'; const PRESS_RETENTION_OFFSET = {top: 20, left: 20, right: 20, bottom: 30}; type TVProps = $ReadOnly<{| hasTVPreferredFocus?: ?boolean, - tvParallaxProperties?: ?Object, + tvParallaxProperties?: ?TVParallaxPropertiesType, |}>; type Props = $ReadOnly<{| @@ -132,9 +132,12 @@ type Props = $ReadOnly<{| */ const TouchableOpacity = ((createReactClass({ displayName: 'TouchableOpacity', - mixins: [TimerMixin, Touchable.Mixin, NativeMethodsMixin], + mixins: [Touchable.Mixin.withoutDefaultFocusAndBlur, NativeMethodsMixin], propTypes: { + /* $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. */ ...TouchableWithoutFeedback.propTypes, /** * Determines what the opacity of the wrapped view should be when touch is @@ -159,7 +162,13 @@ const TouchableOpacity = ((createReactClass({ getInitialState: function() { return { + /* $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. */ ...this.touchableGetInitialState(), + /* $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. */ anim: new Animated.Value(this._getChildStyleOpacityWithDefault()), }; }, @@ -194,7 +203,7 @@ const TouchableOpacity = ((createReactClass({ * `Touchable.Mixin` self callbacks. The mixin will invoke these if they are * defined on your component. */ - touchableHandleActivePressIn: function(e: Event) { + touchableHandleActivePressIn: function(e: PressEvent) { if (e.dispatchConfig.registrationName === 'onResponderGrant') { this._opacityActive(0); } else { @@ -203,16 +212,30 @@ const TouchableOpacity = ((createReactClass({ this.props.onPressIn && this.props.onPressIn(e); }, - touchableHandleActivePressOut: function(e: Event) { + touchableHandleActivePressOut: function(e: PressEvent) { this._opacityInactive(250); this.props.onPressOut && this.props.onPressOut(e); }, - touchableHandlePress: function(e: Event) { + touchableHandleFocus: function(e: Event) { + if (Platform.isTV) { + this._opacityActive(150); + } + this.props.onFocus && this.props.onFocus(e); + }, + + touchableHandleBlur: function(e: Event) { + if (Platform.isTV) { + this._opacityInactive(250); + } + this.props.onBlur && this.props.onBlur(e); + }, + + touchableHandlePress: function(e: PressEvent) { this.props.onPress && this.props.onPress(e); }, - touchableHandleLongPress: function(e: Event) { + touchableHandleLongPress: function(e: PressEvent) { this.props.onLongPress && this.props.onLongPress(e); }, @@ -243,12 +266,15 @@ const TouchableOpacity = ((createReactClass({ }, _opacityInactive: function(duration: number) { + /* $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. */ this.setOpacityTo(this._getChildStyleOpacityWithDefault(), duration); }, _getChildStyleOpacityWithDefault: function() { const childStyle = flattenStyle(this.props.style) || {}; - return childStyle.opacity == undefined ? 1 : childStyle.opacity; + return childStyle.opacity == null ? 1 : childStyle.opacity; }, render: function() { @@ -267,13 +293,31 @@ const TouchableOpacity = ((createReactClass({ hasTVPreferredFocus={this.props.hasTVPreferredFocus} tvParallaxProperties={this.props.tvParallaxProperties} hitSlop={this.props.hitSlop} + /* $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. */ onStartShouldSetResponder={this.touchableHandleStartShouldSetResponder} onResponderTerminationRequest={ + /* $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. */ this.touchableHandleResponderTerminationRequest } + /* $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. */ onResponderGrant={this.touchableHandleResponderGrant} + /* $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. */ onResponderMove={this.touchableHandleResponderMove} + /* $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. */ onResponderRelease={this.touchableHandleResponderRelease} + /* $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. */ onResponderTerminate={this.touchableHandleResponderTerminate}> {this.props.children} {Touchable.renderDebugView({ diff --git a/Libraries/Components/Touchable/TouchableWithoutFeedback.js b/Libraries/Components/Touchable/TouchableWithoutFeedback.js index 06af8ab40a1b7e..be072554dc97b2 100755 --- a/Libraries/Components/Touchable/TouchableWithoutFeedback.js +++ b/Libraries/Components/Touchable/TouchableWithoutFeedback.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -10,47 +10,51 @@ 'use strict'; -const EdgeInsetsPropType = require('EdgeInsetsPropType'); +const DeprecatedEdgeInsetsPropType = require('DeprecatedEdgeInsetsPropType'); const React = require('React'); const PropTypes = require('prop-types'); -const TimerMixin = require('react-timer-mixin'); const Touchable = require('Touchable'); +const View = require('View'); const createReactClass = require('create-react-class'); const ensurePositiveDelayProps = require('ensurePositiveDelayProps'); -const warning = require('fbjs/lib/warning'); const { - AccessibilityComponentTypes, - AccessibilityRoles, - AccessibilityStates, - AccessibilityTraits, -} = require('ViewAccessibility'); + DeprecatedAccessibilityComponentTypes, + DeprecatedAccessibilityRoles, + DeprecatedAccessibilityStates, + DeprecatedAccessibilityTraits, +} = require('DeprecatedViewAccessibility'); -import type {PressEvent} from 'CoreEventTypes'; +import type {SyntheticEvent, LayoutEvent, PressEvent} from 'CoreEventTypes'; import type {EdgeInsetsProp} from 'EdgeInsetsPropType'; import type { AccessibilityComponentType, AccessibilityRole, - AccessibilityStates as AccessibilityStatesFlow, - AccessibilityTraits as AccessibilityTraitsFlow, + AccessibilityStates, + AccessibilityTraits, } from 'ViewAccessibility'; +type TargetEvent = SyntheticEvent< + $ReadOnly<{| + target: number, + |}>, +>; + +type BlurEvent = TargetEvent; +type FocusEvent = TargetEvent; + const PRESS_RETENTION_OFFSET = {top: 20, left: 20, right: 20, bottom: 30}; export type Props = $ReadOnly<{| accessible?: ?boolean, accessibilityComponentType?: ?AccessibilityComponentType, - accessibilityLabel?: - | null - | React$PropType$Primitive - | string - | Array - | any, - accessibilityHint?: string, + accessibilityLabel?: ?Stringish, + accessibilityHint?: ?Stringish, + accessibilityIgnoresInvertColors?: ?boolean, accessibilityRole?: ?AccessibilityRole, - accessibilityStates?: ?AccessibilityStatesFlow, - accessibilityTraits?: ?AccessibilityTraitsFlow, + accessibilityStates?: ?AccessibilityStates, + accessibilityTraits?: ?AccessibilityTraits, children?: ?React.Node, delayLongPress?: ?number, delayPressIn?: ?number, @@ -58,11 +62,13 @@ export type Props = $ReadOnly<{| disabled?: ?boolean, hitSlop?: ?EdgeInsetsProp, nativeID?: ?string, - onLayout?: ?Function, - onLongPress?: ?Function, - onPress?: ?Function, - onPressIn?: ?Function, - onPressOut?: ?Function, + onBlur?: ?(e: BlurEvent) => void, + onFocus?: ?(e: FocusEvent) => void, + onLayout?: ?(event: LayoutEvent) => mixed, + onLongPress?: ?(event: PressEvent) => mixed, + onPress?: ?(event: PressEvent) => mixed, + onPressIn?: ?(event: PressEvent) => mixed, + onPressOut?: ?(event: PressEvent) => mixed, pressRetentionOffset?: ?EdgeInsetsProp, rejectResponderTermination?: ?boolean, testID?: ?string, @@ -77,20 +83,22 @@ export type Props = $ReadOnly<{| */ const TouchableWithoutFeedback = ((createReactClass({ displayName: 'TouchableWithoutFeedback', - mixins: [TimerMixin, Touchable.Mixin], + mixins: [Touchable.Mixin], propTypes: { accessible: PropTypes.bool, accessibilityLabel: PropTypes.node, accessibilityHint: PropTypes.string, - accessibilityComponentType: PropTypes.oneOf(AccessibilityComponentTypes), - accessibilityRole: PropTypes.oneOf(AccessibilityRoles), + accessibilityComponentType: PropTypes.oneOf( + DeprecatedAccessibilityComponentTypes, + ), + accessibilityRole: PropTypes.oneOf(DeprecatedAccessibilityRoles), accessibilityStates: PropTypes.arrayOf( - PropTypes.oneOf(AccessibilityStates), + PropTypes.oneOf(DeprecatedAccessibilityStates), ), accessibilityTraits: PropTypes.oneOfType([ - PropTypes.oneOf(AccessibilityTraits), - PropTypes.arrayOf(PropTypes.oneOf(AccessibilityTraits)), + PropTypes.oneOf(DeprecatedAccessibilityTraits), + PropTypes.arrayOf(PropTypes.oneOf(DeprecatedAccessibilityTraits)), ]), /** * When `accessible` is true (which is the default) this may be called when @@ -153,7 +161,7 @@ const TouchableWithoutFeedback = ((createReactClass({ * reactivated! Move it back and forth several times while the scroll view * is disabled. Ensure you pass in a constant to reduce memory allocations. */ - pressRetentionOffset: EdgeInsetsPropType, + pressRetentionOffset: DeprecatedEdgeInsetsPropType, /** * This defines how far your touch can start away from the button. This is * added to `pressRetentionOffset` when moving off of the button. @@ -162,7 +170,7 @@ const TouchableWithoutFeedback = ((createReactClass({ * of sibling views always takes precedence if a touch hits two overlapping * views. */ - hitSlop: EdgeInsetsPropType, + hitSlop: DeprecatedEdgeInsetsPropType, }, getInitialState: function() { @@ -225,28 +233,12 @@ const TouchableWithoutFeedback = ((createReactClass({ // $FlowFixMe(>=0.41.0) const child = React.Children.only(this.props.children); let children = child.props.children; - warning( - !child.type || child.type.displayName !== 'Text', - 'TouchableWithoutFeedback does not work well with Text children. Wrap children in a View instead. See ' + - ((child._owner && child._owner.getName && child._owner.getName()) || - ''), - ); - if ( - Touchable.TOUCH_TARGET_DEBUG && - child.type && - child.type.displayName === 'View' - ) { + if (Touchable.TOUCH_TARGET_DEBUG && child.type === View) { children = React.Children.toArray(children); children.push( Touchable.renderDebugView({color: 'red', hitSlop: this.props.hitSlop}), ); } - const style = - Touchable.TOUCH_TARGET_DEBUG && - child.type && - child.type.displayName === 'Text' - ? [child.props.style, {color: 'red'}] - : child.props.style; return (React: any).cloneElement(child, { accessible: this.props.accessible !== false, accessibilityLabel: this.props.accessibilityLabel, @@ -266,7 +258,6 @@ const TouchableWithoutFeedback = ((createReactClass({ onResponderMove: this.touchableHandleResponderMove, onResponderRelease: this.touchableHandleResponderRelease, onResponderTerminate: this.touchableHandleResponderTerminate, - style, children, }); }, diff --git a/Libraries/Components/Touchable/__mocks__/ensureComponentIsNative.js b/Libraries/Components/Touchable/__mocks__/ensureComponentIsNative.js index f152b68bba74a4..234c178afddd94 100644 --- a/Libraries/Components/Touchable/__mocks__/ensureComponentIsNative.js +++ b/Libraries/Components/Touchable/__mocks__/ensureComponentIsNative.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/Components/Touchable/__tests__/TouchableHighlight-test.js b/Libraries/Components/Touchable/__tests__/TouchableHighlight-test.js index f40cc0b7ad1be9..8f299d8a360103 100644 --- a/Libraries/Components/Touchable/__tests__/TouchableHighlight-test.js +++ b/Libraries/Components/Touchable/__tests__/TouchableHighlight-test.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/Components/Touchable/ensureComponentIsNative.js b/Libraries/Components/Touchable/ensureComponentIsNative.js index 5ce5a39d9baaf0..dbf166e4b050a4 100644 --- a/Libraries/Components/Touchable/ensureComponentIsNative.js +++ b/Libraries/Components/Touchable/ensureComponentIsNative.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -10,7 +10,7 @@ 'use strict'; -const invariant = require('fbjs/lib/invariant'); +const invariant = require('invariant'); const ensureComponentIsNative = function(component: any) { invariant( diff --git a/Libraries/Components/Touchable/ensurePositiveDelayProps.js b/Libraries/Components/Touchable/ensurePositiveDelayProps.js index 01c001ad4b6a7e..69dad5539faf2b 100644 --- a/Libraries/Components/Touchable/ensurePositiveDelayProps.js +++ b/Libraries/Components/Touchable/ensurePositiveDelayProps.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -10,7 +10,7 @@ 'use strict'; -const invariant = require('fbjs/lib/invariant'); +const invariant = require('invariant'); const ensurePositiveDelayProps = function(props: any) { invariant( diff --git a/Libraries/Components/UnimplementedViews/UnimplementedView.js b/Libraries/Components/UnimplementedViews/UnimplementedView.js index 7be6f1ac77f759..30e64eb83d01fd 100644 --- a/Libraries/Components/UnimplementedViews/UnimplementedView.js +++ b/Libraries/Components/UnimplementedViews/UnimplementedView.js @@ -1,10 +1,10 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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 + * @flow strict-local * @format */ 'use strict'; diff --git a/Libraries/Components/View/PlatformViewPropTypes.android.js b/Libraries/Components/View/PlatformViewPropTypes.android.js new file mode 100644 index 00000000000000..f96fe3a62d8149 --- /dev/null +++ b/Libraries/Components/View/PlatformViewPropTypes.android.js @@ -0,0 +1,14 @@ +/** + * 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. + * + * @providesModule PlatformViewPropTypes + * @format + * @flow + */ + +'use strict'; + +export type PlatformViewPropTypes = {}; diff --git a/Libraries/Components/View/PlatformViewPropTypes.ios.js b/Libraries/Components/View/PlatformViewPropTypes.ios.js new file mode 100644 index 00000000000000..fc842750601026 --- /dev/null +++ b/Libraries/Components/View/PlatformViewPropTypes.ios.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. + * + * @providesModule PlatformViewPropTypes + * @format + * @flow + */ + +'use strict'; + +import type {TVViewProps} from 'TVViewPropTypes'; + +export type PlatformViewPropTypes = TVViewProps; diff --git a/Libraries/Components/View/PlatformViewPropTypes.js b/Libraries/Components/View/PlatformViewPropTypes.js deleted file mode 100644 index 4c6105893cc6eb..00000000000000 --- a/Libraries/Components/View/PlatformViewPropTypes.js +++ /dev/null @@ -1,21 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @format - * @flow - */ - -const Platform = require('Platform'); - -let TVViewPropTypes = {}; -// We need to always include TVViewPropTypes on Android -// as unlike on iOS we can't detect TV devices at build time -// and hence make view manager export a different list of native properties. -if (Platform.isTV || Platform.OS === 'android') { - TVViewPropTypes = require('TVViewPropTypes'); -} - -module.exports = TVViewPropTypes; diff --git a/Libraries/Components/View/ReactNativeStyleAttributes.js b/Libraries/Components/View/ReactNativeStyleAttributes.js index 7a75dda8b7f41b..240e6a4f0a4e4a 100644 --- a/Libraries/Components/View/ReactNativeStyleAttributes.js +++ b/Libraries/Components/View/ReactNativeStyleAttributes.js @@ -1,32 +1,32 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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 + * @format strict-local * @flow */ 'use strict'; -const ImageStylePropTypes = require('ImageStylePropTypes'); +const DeprecatedImageStylePropTypes = require('DeprecatedImageStylePropTypes'); const TextStylePropTypes = require('TextStylePropTypes'); -const ViewStylePropTypes = require('ViewStylePropTypes'); +const DeprecatedViewStylePropTypes = require('DeprecatedViewStylePropTypes'); -/* $FlowFixMe(>=0.54.0 site=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. */ -const keyMirror = require('fbjs/lib/keyMirror'); const processColor = require('processColor'); const processTransform = require('processTransform'); const sizesDiffer = require('sizesDiffer'); -const ReactNativeStyleAttributes = { - ...keyMirror(ViewStylePropTypes), - ...keyMirror(TextStylePropTypes), - ...keyMirror(ImageStylePropTypes), -}; +const ReactNativeStyleAttributes = {}; + +for (const attributeName of Object.keys({ + ...DeprecatedViewStylePropTypes, + ...TextStylePropTypes, + ...DeprecatedImageStylePropTypes, +})) { + ReactNativeStyleAttributes[attributeName] = true; +} ReactNativeStyleAttributes.transform = {process: processTransform}; ReactNativeStyleAttributes.shadowOffset = {diff: sizesDiffer}; diff --git a/Libraries/Components/View/ReactNativeViewAttributes.js b/Libraries/Components/View/ReactNativeViewAttributes.js index 889145dfe6da06..3ab651c37778b3 100644 --- a/Libraries/Components/View/ReactNativeViewAttributes.js +++ b/Libraries/Components/View/ReactNativeViewAttributes.js @@ -1,11 +1,11 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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 - * @flow strict */ 'use strict'; @@ -34,6 +34,7 @@ ReactNativeViewAttributes.UIView = { onAccessibilityAction: true, onAccessibilityTap: true, onMagicTap: true, + onAccessibilityEscape: true, collapsable: true, needsOffscreenAlphaCompositing: true, style: ReactNativeStyleAttributes, diff --git a/Libraries/Components/View/ShadowPropTypesIOS.js b/Libraries/Components/View/ShadowPropTypesIOS.js deleted file mode 100644 index 2dfc021735ad9e..00000000000000 --- a/Libraries/Components/View/ShadowPropTypesIOS.js +++ /dev/null @@ -1,50 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @flow - * @format - */ -'use strict'; - -const ColorPropType = require('ColorPropType'); -const ReactPropTypes = require('prop-types'); - -/** - * These props can be used to dynamically generate shadows on views, images, text, etc. - * - * Because they are dynamically generated, they may cause performance regressions. Static - * shadow image asset may be a better way to go for optimal performance. - * - * These properties are iOS only - for similar functionality on Android, use the [`elevation` - * property](docs/viewstyleproptypes.html#elevation). - */ -const ShadowPropTypesIOS = { - /** - * Sets the drop shadow color - * @platform ios - */ - shadowColor: ColorPropType, - /** - * Sets the drop shadow offset - * @platform ios - */ - shadowOffset: ReactPropTypes.shape({ - width: ReactPropTypes.number, - height: ReactPropTypes.number, - }), - /** - * Sets the drop shadow opacity (multiplied by the color's alpha component) - * @platform ios - */ - shadowOpacity: ReactPropTypes.number, - /** - * Sets the drop shadow blur radius - * @platform ios - */ - shadowRadius: ReactPropTypes.number, -}; - -module.exports = ShadowPropTypesIOS; diff --git a/Libraries/Components/View/View.js b/Libraries/Components/View/View.js index 2cfa05a8c118b2..3e9d7c10a1a7b6 100644 --- a/Libraries/Components/View/View.js +++ b/Libraries/Components/View/View.js @@ -1,22 +1,21 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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 + * @flow strict-local */ 'use strict'; const React = require('React'); const TextAncestor = require('TextAncestor'); +const ViewNativeComponent = require('ViewNativeComponent'); -const invariant = require('fbjs/lib/invariant'); -const requireNativeComponent = require('requireNativeComponent'); +const invariant = require('invariant'); -import type {NativeComponent} from 'ReactNative'; import type {ViewProps} from 'ViewPropTypes'; export type Props = ViewProps; @@ -28,27 +27,29 @@ export type Props = ViewProps; * * @see http://facebook.github.io/react-native/docs/view.html */ -const RCTView = requireNativeComponent('RCTView'); -let ViewToExport = RCTView; +let ViewToExport = ViewNativeComponent; if (__DEV__) { - const View = (props: Props, forwardedRef: ?React.Ref<'RCTView'>) => { - return ( - - {hasTextAncestor => { - invariant( - !hasTextAncestor, - 'Nesting of within is not currently supported.', - ); - return ; - }} - - ); - }; - // $FlowFixMe - TODO T29156721 `React.forwardRef` is not defined in Flow, yet. - ViewToExport = React.forwardRef(View); + if (!global.__RCTProfileIsProfiling) { + const View = ( + props: Props, + forwardedRef: React.Ref, + ) => { + return ( + + {hasTextAncestor => { + invariant( + !hasTextAncestor, + 'Nesting of within is not currently supported.', + ); + return ; + }} + + ); + }; + ViewToExport = React.forwardRef(View); + ViewToExport.displayName = 'View'; + } } -module.exports = ((ViewToExport: $FlowFixMe): Class< - NativeComponent, ->); +module.exports = ((ViewToExport: $FlowFixMe): typeof ViewNativeComponent); diff --git a/Libraries/Components/View/ViewAccessibility.js b/Libraries/Components/View/ViewAccessibility.js index c3512c42c4eb81..ed7eb4d68e50b8 100644 --- a/Libraries/Components/View/ViewAccessibility.js +++ b/Libraries/Components/View/ViewAccessibility.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -39,6 +39,7 @@ export type AccessibilityComponentType = | 'radiobutton_checked' | 'radiobutton_unchecked'; +// This must be kept in sync with the AccessibilityRolesMask in RCTViewManager.m export type AccessibilityRole = | 'none' | 'button' @@ -52,50 +53,5 @@ export type AccessibilityRole = | 'header' | 'summary'; -export type AccessibilityState = 'selected' | 'disabled'; - -export type AccessibilityStates = - | AccessibilityState - | $ReadOnlyArray; - -module.exports = { - AccessibilityTraits: [ - 'none', - 'button', - 'link', - 'header', - 'search', - 'image', - 'selected', - 'plays', - 'key', - 'text', - 'summary', - 'disabled', - 'frequentUpdates', - 'startsMedia', - 'adjustable', - 'allowsDirectInteraction', - 'pageTurn', - ], - AccessibilityComponentTypes: [ - 'none', - 'button', - 'radiobutton_checked', - 'radiobutton_unchecked', - ], - AccessibilityRoles: [ - 'none', - 'button', - 'link', - 'search', - 'image', - 'keyboardkey', - 'text', - 'adjustable', - 'imagebutton', - 'header', - 'summary', - ], - AccessibilityStates: ['selected', 'disabled'], -}; +// This must be kept in sync with the AccessibilityStatesMask in RCTViewManager.m +export type AccessibilityStates = $ReadOnlyArray<'disabled' | 'selected'>; diff --git a/Libraries/Components/View/ViewNativeComponent.js b/Libraries/Components/View/ViewNativeComponent.js new file mode 100644 index 00000000000000..432040ad3c3a4c --- /dev/null +++ b/Libraries/Components/View/ViewNativeComponent.js @@ -0,0 +1,23 @@ +/** + * 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 ReactNative = require('ReactNative'); + +const requireNativeComponent = require('requireNativeComponent'); + +import type {ViewProps} from 'ViewPropTypes'; + +type ViewNativeComponentType = Class>; + +const NativeViewComponent = requireNativeComponent('RCTView'); + +module.exports = ((NativeViewComponent: any): ViewNativeComponentType); diff --git a/Libraries/Components/View/ViewPropTypes.js b/Libraries/Components/View/ViewPropTypes.js index fb8b27a36afc64..259dd91ea91da0 100644 --- a/Libraries/Components/View/ViewPropTypes.js +++ b/Libraries/Components/View/ViewPropTypes.js @@ -1,255 +1,59 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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 + * @flow strict-local */ 'use strict'; -const React = require('React'); -const EdgeInsetsPropType = require('EdgeInsetsPropType'); -const PlatformViewPropTypes = require('PlatformViewPropTypes'); -const PropTypes = require('prop-types'); -const StyleSheetPropType = require('StyleSheetPropType'); -const ViewStylePropTypes = require('ViewStylePropTypes'); - -const { - AccessibilityComponentTypes, - AccessibilityTraits, - AccessibilityRoles, - AccessibilityStates, -} = require('ViewAccessibility'); - +import type {PressEvent, Layout, LayoutEvent} from 'CoreEventTypes'; +import type {EdgeInsetsProp} from 'EdgeInsetsPropType'; +import type React from 'React'; +import type {ViewStyleProp} from 'StyleSheet'; +import type {TVViewProps} from 'TVViewPropTypes'; import type { AccessibilityComponentType, AccessibilityTrait, AccessibilityRole, - AccessibilityState, + AccessibilityStates, } from 'ViewAccessibility'; -import type {EdgeInsetsProp} from 'EdgeInsetsPropType'; -import type {TVViewProps} from 'TVViewPropTypes'; -import type {Layout, LayoutEvent} from 'CoreEventTypes'; - -const stylePropType = StyleSheetPropType(ViewStylePropTypes); export type ViewLayout = Layout; export type ViewLayoutEvent = LayoutEvent; type DirectEventProps = $ReadOnly<{| - onAccessibilityAction?: Function, - onAccessibilityTap?: Function, - onLayout?: ?(event: LayoutEvent) => void, - onMagicTap?: Function, -|}>; - -type TouchEventProps = $ReadOnly<{| - onTouchCancel?: ?Function, - onTouchCancelCapture?: ?Function, - onTouchEnd?: ?Function, - onTouchEndCapture?: ?Function, - onTouchMove?: ?Function, - onTouchMoveCapture?: ?Function, - onTouchStart?: ?Function, - onTouchStartCapture?: ?Function, -|}>; - -type GestureResponderEventProps = $ReadOnly<{| - onMoveShouldSetResponder?: ?Function, - onMoveShouldSetResponderCapture?: ?Function, - onResponderGrant?: ?Function, - onResponderMove?: ?Function, - onResponderReject?: ?Function, - onResponderRelease?: ?Function, - onResponderStart?: ?Function, - onResponderTerminate?: ?Function, - onResponderTerminationRequest?: ?Function, - onStartShouldSetResponder?: ?Function, - onStartShouldSetResponderCapture?: ?Function, -|}>; - -export type ViewProps = $ReadOnly<{| - ...DirectEventProps, - ...GestureResponderEventProps, - ...TouchEventProps, - - // There's no easy way to create a different type if (Platform.isTV): - // so we must include TVViewProps - ...TVViewProps, - - accessible?: boolean, - accessibilityLabel?: - | null - | React$PropType$Primitive - | string - | Array - | any, - accessibilityHint?: string, - accessibilityActions?: Array, - accessibilityComponentType?: AccessibilityComponentType, - accessibilityLiveRegion?: 'none' | 'polite' | 'assertive', - importantForAccessibility?: 'auto' | 'yes' | 'no' | 'no-hide-descendants', - accessibilityIgnoresInvertColors?: boolean, - accessibilityTraits?: AccessibilityTrait | Array, - accessibilityRole?: AccessibilityRole, - accessibilityStates?: Array, - accessibilityViewIsModal?: boolean, - accessibilityElementsHidden?: boolean, - children?: ?React.Node, - testID?: ?string, - nativeID?: string, - hitSlop?: ?EdgeInsetsProp, - pointerEvents?: null | 'box-none' | 'none' | 'box-only' | 'auto', - style?: stylePropType, - removeClippedSubviews?: boolean, - renderToHardwareTextureAndroid?: boolean, - shouldRasterizeIOS?: boolean, - collapsable?: boolean, - needsOffscreenAlphaCompositing?: boolean, -|}>; - -module.exports = { - /** - * When `true`, indicates that the view is an accessibility element. - * By default, all the touchable elements are accessible. - * - * See http://facebook.github.io/react-native/docs/view.html#accessible - */ - accessible: PropTypes.bool, - - /** - * Overrides the text that's read by the screen reader when the user interacts - * with the element. By default, the label is constructed by traversing all - * the children and accumulating all the `Text` nodes separated by space. - * - * See http://facebook.github.io/react-native/docs/view.html#accessibilitylabel - */ - accessibilityLabel: PropTypes.node, - - /** - * An accessibility hint helps users understand what will happen when they perform - * an action on the accessibility element when that result is not obvious from the - * accessibility label. - * - * @platform ios - * - * See http://facebook.github.io/react-native/docs/view.html#accessibilityHint - */ - accessibilityHint: PropTypes.string, - - /** - * Provides an array of custom actions available for accessibility. - * - * @platform ios - */ - accessibilityActions: PropTypes.arrayOf(PropTypes.string), - - /** - * Prevents view from being inverted if set to true and color inversion is turned on. - * - * @platform ios - */ - accessibilityIgnoresInvertColors: PropTypes.bool, - - /** - * Indicates to accessibility services to treat UI component like a - * native one. Works for Android only. - * - * @platform android - * - * See http://facebook.github.io/react-native/docs/view.html#accessibilitycomponenttype - */ - accessibilityComponentType: PropTypes.oneOf(AccessibilityComponentTypes), - - /** - * Indicates to accessibility services to treat UI component like a - */ - accessibilityRole: PropTypes.oneOf(AccessibilityRoles), - - /** - * Indicates to accessibility services that UI Component is in a specific State. - */ - accessibilityStates: PropTypes.arrayOf(PropTypes.oneOf(AccessibilityStates)), - /** - * Indicates to accessibility services whether the user should be notified - * when this view changes. Works for Android API >= 19 only. - * - * @platform android - * - * See http://facebook.github.io/react-native/docs/view.html#accessibilityliveregion - */ - accessibilityLiveRegion: PropTypes.oneOf(['none', 'polite', 'assertive']), - /** - * Controls how view is important for accessibility which is if it - * fires accessibility events and if it is reported to accessibility services - * that query the screen. Works for Android only. - * - * @platform android - * - * See http://facebook.github.io/react-native/docs/view.html#importantforaccessibility - */ - importantForAccessibility: PropTypes.oneOf([ - 'auto', - 'yes', - 'no', - 'no-hide-descendants', - ]), - - /** - * Provides additional traits to screen reader. By default no traits are - * provided unless specified otherwise in element. - * - * You can provide one trait or an array of many traits. + * When `accessible` is true, the system will try to invoke this function + * when the user performs an accessibility custom action. * * @platform ios - * - * See http://facebook.github.io/react-native/docs/view.html#accessibilitytraits */ - accessibilityTraits: PropTypes.oneOfType([ - PropTypes.oneOf(AccessibilityTraits), - PropTypes.arrayOf(PropTypes.oneOf(AccessibilityTraits)), - ]), + onAccessibilityAction?: ?(string) => void, /** - * A value indicating whether VoiceOver should ignore the elements - * within views that are siblings of the receiver. - * Default is `false`. - * - * @platform ios + * When `accessible` is true, the system will try to invoke this function + * when the user performs accessibility tap gesture. * - * See http://facebook.github.io/react-native/docs/view.html#accessibilityviewismodal + * See http://facebook.github.io/react-native/docs/view.html#onaccessibilitytap */ - accessibilityViewIsModal: PropTypes.bool, + onAccessibilityTap?: ?() => void, /** - * A value indicating whether the accessibility elements contained within - * this accessibility element are hidden. - * - * @platform ios + * Invoked on mount and layout changes with: * - * See http://facebook.github.io/react-native/docs/view.html#accessibilityElementsHidden - */ - accessibilityElementsHidden: PropTypes.bool, - - /** - * When `accessible` is true, the system will try to invoke this function - * when the user performs an accessibility custom action. + * `{nativeEvent: { layout: {x, y, width, height}}}` * - * @platform ios - */ - onAccessibilityAction: PropTypes.func, - - /** - * When `accessible` is true, the system will try to invoke this function - * when the user performs accessibility tap gesture. + * This event is fired immediately once the layout has been calculated, but + * the new layout may not yet be reflected on the screen at the time the + * event is received, especially if a layout animation is in progress. * - * See http://facebook.github.io/react-native/docs/view.html#onaccessibilitytap + * See http://facebook.github.io/react-native/docs/view.html#onlayout */ - onAccessibilityTap: PropTypes.func, + onLayout?: ?(event: LayoutEvent) => mixed, /** * When `accessible` is `true`, the system will invoke this function when the @@ -257,31 +61,55 @@ module.exports = { * * See http://facebook.github.io/react-native/docs/view.html#onmagictap */ - onMagicTap: PropTypes.func, + onMagicTap?: ?() => void, /** - * Used to locate this view in end-to-end tests. - * - * > This disables the 'layout-only view removal' optimization for this view! + * When `accessible` is `true`, the system will invoke this function when the + * user performs the escape gesture. * - * See http://facebook.github.io/react-native/docs/view.html#testid + * See http://facebook.github.io/react-native/docs/view.html#onaccessibilityescape */ - testID: PropTypes.string, + onAccessibilityEscape?: ?() => void, +|}>; +type TouchEventProps = $ReadOnly<{| + onTouchCancel?: ?(e: PressEvent) => void, + onTouchCancelCapture?: ?(e: PressEvent) => void, + onTouchEnd?: ?(e: PressEvent) => void, + onTouchEndCapture?: ?(e: PressEvent) => void, + onTouchMove?: ?(e: PressEvent) => void, + onTouchMoveCapture?: ?(e: PressEvent) => void, + onTouchStart?: ?(e: PressEvent) => void, + onTouchStartCapture?: ?(e: PressEvent) => void, +|}>; + +/** + * For most touch interactions, you'll simply want to wrap your component in + * `TouchableHighlight` or `TouchableOpacity`. Check out `Touchable.js`, + * `ScrollResponder.js` and `ResponderEventPlugin.js` for more discussion. + */ +type GestureResponderEventProps = $ReadOnly<{| /** - * Used to locate this view from native classes. + * Does this view want to "claim" touch responsiveness? This is called for + * every touch move on the `View` when it is not the responder. * - * > This disables the 'layout-only view removal' optimization for this view! + * `View.props.onMoveShouldSetResponder: (event) => [true | false]`, where + * `event` is a synthetic touch event as described above. * - * See http://facebook.github.io/react-native/docs/view.html#nativeid + * See http://facebook.github.io/react-native/docs/view.html#onmoveshouldsetresponder */ - nativeID: PropTypes.string, + onMoveShouldSetResponder?: ?(e: PressEvent) => boolean, /** - * For most touch interactions, you'll simply want to wrap your component in - * `TouchableHighlight` or `TouchableOpacity`. Check out `Touchable.js`, - * `ScrollResponder.js` and `ResponderEventPlugin.js` for more discussion. + * If a parent `View` wants to prevent a child `View` from becoming responder + * on a move, it should have this handler which returns `true`. + * + * `View.props.onMoveShouldSetResponderCapture: (event) => [true | false]`, + * where `event` is a synthetic touch event as described above. + * + * See http://facebook.github.io/react-native/docs/view.html#onMoveShouldsetrespondercapture */ + onMoveShouldSetResponderCapture?: ?(e: PressEvent) => boolean, /** * The View is now responding for touch events. This is the time to highlight @@ -290,9 +118,12 @@ module.exports = { * `View.props.onResponderGrant: (event) => {}`, where `event` is a synthetic * touch event as described above. * + * PanResponder includes a note `// TODO: t7467124 investigate if this can be removed` that + * should help fixing this return type. + * * See http://facebook.github.io/react-native/docs/view.html#onrespondergrant */ - onResponderGrant: PropTypes.func, + onResponderGrant?: ?(e: PressEvent) => void | boolean, /** * The user is moving their finger. @@ -302,7 +133,7 @@ module.exports = { * * See http://facebook.github.io/react-native/docs/view.html#onrespondermove */ - onResponderMove: PropTypes.func, + onResponderMove?: ?(e: PressEvent) => void, /** * Another responder is already active and will not release it to that `View` @@ -313,7 +144,7 @@ module.exports = { * * See http://facebook.github.io/react-native/docs/view.html#onresponderreject */ - onResponderReject: PropTypes.func, + onResponderReject?: ?(e: PressEvent) => void, /** * Fired at the end of the touch. @@ -323,7 +154,10 @@ module.exports = { * * See http://facebook.github.io/react-native/docs/view.html#onresponderrelease */ - onResponderRelease: PropTypes.func, + onResponderRelease?: ?(e: PressEvent) => void, + + onResponderStart?: ?(e: PressEvent) => void, + onResponderEnd?: ?(e: PressEvent) => void, /** * The responder has been taken from the `View`. Might be taken by other @@ -336,7 +170,7 @@ module.exports = { * * See http://facebook.github.io/react-native/docs/view.html#onresponderterminate */ - onResponderTerminate: PropTypes.func, + onResponderTerminate?: ?(e: PressEvent) => void, /** * Some other `View` wants to become responder and is asking this `View` to @@ -347,7 +181,7 @@ module.exports = { * * See http://facebook.github.io/react-native/docs/view.html#onresponderterminationrequest */ - onResponderTerminationRequest: PropTypes.func, + onResponderTerminationRequest?: ?(e: PressEvent) => boolean, /** * Does this view want to become responder on the start of a touch? @@ -357,7 +191,7 @@ module.exports = { * * See http://facebook.github.io/react-native/docs/view.html#onstartshouldsetresponder */ - onStartShouldSetResponder: PropTypes.func, + onStartShouldSetResponder?: ?(e: PressEvent) => boolean, /** * If a parent `View` wants to prevent a child `View` from becoming responder @@ -368,89 +202,140 @@ module.exports = { * * See http://facebook.github.io/react-native/docs/view.html#onstartshouldsetrespondercapture */ - onStartShouldSetResponderCapture: PropTypes.func, + onStartShouldSetResponderCapture?: ?(e: PressEvent) => boolean, +|}>; + +type AndroidDrawableThemeAttr = $ReadOnly<{| + type: 'ThemeAttrAndroid', + attribute: string, +|}>; + +type AndroidDrawableRipple = $ReadOnly<{| + type: 'RippleAndroid', + color?: ?number, + borderless?: ?boolean, +|}>; + +type AndroidDrawable = AndroidDrawableThemeAttr | AndroidDrawableRipple; + +type AndroidViewProps = $ReadOnly<{| + nativeBackgroundAndroid?: ?AndroidDrawable, + nativeForegroundAndroid?: ?AndroidDrawable, /** - * Does this view want to "claim" touch responsiveness? This is called for - * every touch move on the `View` when it is not the responder. + * Whether this `View` should render itself (and all of its children) into a + * single hardware texture on the GPU. * - * `View.props.onMoveShouldSetResponder: (event) => [true | false]`, where - * `event` is a synthetic touch event as described above. + * @platform android * - * See http://facebook.github.io/react-native/docs/view.html#onmoveshouldsetresponder + * See http://facebook.github.io/react-native/docs/view.html#rendertohardwaretextureandroid */ - onMoveShouldSetResponder: PropTypes.func, + renderToHardwareTextureAndroid?: ?boolean, /** - * If a parent `View` wants to prevent a child `View` from becoming responder - * on a move, it should have this handler which returns `true`. + * Views that are only used to layout their children or otherwise don't draw + * anything may be automatically removed from the native hierarchy as an + * optimization. Set this property to `false` to disable this optimization and + * ensure that this `View` exists in the native view hierarchy. * - * `View.props.onMoveShouldSetResponderCapture: (event) => [true | false]`, - * where `event` is a synthetic touch event as described above. + * @platform android * - * See http://facebook.github.io/react-native/docs/view.html#onMoveShouldsetrespondercapture + * See http://facebook.github.io/react-native/docs/view.html#collapsable */ - onMoveShouldSetResponderCapture: PropTypes.func, + collapsable?: ?boolean, /** - * This defines how far a touch event can start away from the view. - * Typical interface guidelines recommend touch targets that are at least - * 30 - 40 points/density-independent pixels. + * Whether this `View` needs to rendered offscreen and composited with an + * alpha in order to preserve 100% correct colors and blending behavior. * - * > The touch area never extends past the parent view bounds and the Z-index - * > of sibling views always takes precedence if a touch hits two overlapping - * > views. + * @platform android * - * See http://facebook.github.io/react-native/docs/view.html#hitslop + * See http://facebook.github.io/react-native/docs/view.html#needsoffscreenalphacompositing */ - hitSlop: EdgeInsetsPropType, + needsOffscreenAlphaCompositing?: ?boolean, /** - * Invoked on mount and layout changes with: + * Indicates to accessibility services to treat UI component like a + * native one. Works for Android only. * - * `{nativeEvent: { layout: {x, y, width, height}}}` + * @platform android * - * This event is fired immediately once the layout has been calculated, but - * the new layout may not yet be reflected on the screen at the time the - * event is received, especially if a layout animation is in progress. + * See http://facebook.github.io/react-native/docs/view.html#accessibilitycomponenttype + */ + accessibilityComponentType?: ?AccessibilityComponentType, + + /** + * Indicates to accessibility services whether the user should be notified + * when this view changes. Works for Android API >= 19 only. * - * See http://facebook.github.io/react-native/docs/view.html#onlayout + * @platform android + * + * See http://facebook.github.io/react-native/docs/view.html#accessibilityliveregion */ - onLayout: PropTypes.func, + accessibilityLiveRegion?: ?('none' | 'polite' | 'assertive'), /** - * Controls whether the `View` can be the target of touch events. + * Controls how view is important for accessibility which is if it + * fires accessibility events and if it is reported to accessibility services + * that query the screen. Works for Android only. * - * See http://facebook.github.io/react-native/docs/view.html#pointerevents + * @platform android + * + * See http://facebook.github.io/react-native/docs/view.html#importantforaccessibility */ - pointerEvents: PropTypes.oneOf(['box-none', 'none', 'box-only', 'auto']), + importantForAccessibility?: ?('auto' | 'yes' | 'no' | 'no-hide-descendants'), +|}>; +type IOSViewProps = $ReadOnly<{| /** - * See http://facebook.github.io/react-native/docs/style.html + * Provides an array of custom actions available for accessibility. + * + * @platform ios */ - style: stylePropType, + accessibilityActions?: ?$ReadOnlyArray, /** - * This is a special performance property exposed by `RCTView` and is useful - * for scrolling content when there are many subviews, most of which are - * offscreen. For this property to be effective, it must be applied to a - * view that contains many subviews that extend outside its bound. The - * subviews must also have `overflow: hidden`, as should the containing view - * (or one of its superviews). + * Prevents view from being inverted if set to true and color inversion is turned on. * - * See http://facebook.github.io/react-native/docs/view.html#removeclippedsubviews + * @platform ios */ - removeClippedSubviews: PropTypes.bool, + accessibilityIgnoresInvertColors?: ?boolean, /** - * Whether this `View` should render itself (and all of its children) into a - * single hardware texture on the GPU. + * Provides additional traits to screen reader. By default no traits are + * provided unless specified otherwise in element. * - * @platform android + * You can provide one trait or an array of many traits. * - * See http://facebook.github.io/react-native/docs/view.html#rendertohardwaretextureandroid + * @platform ios + * + * See http://facebook.github.io/react-native/docs/view.html#accessibilitytraits + */ + accessibilityTraits?: ?( + | AccessibilityTrait + | $ReadOnlyArray + ), + + /** + * A value indicating whether VoiceOver should ignore the elements + * within views that are siblings of the receiver. + * Default is `false`. + * + * @platform ios + * + * See http://facebook.github.io/react-native/docs/view.html#accessibilityviewismodal */ - renderToHardwareTextureAndroid: PropTypes.bool, + accessibilityViewIsModal?: ?boolean, + + /** + * A value indicating whether the accessibility elements contained within + * this accessibility element are hidden. + * + * @platform ios + * + * See http://facebook.github.io/react-native/docs/view.html#accessibilityElementsHidden + */ + accessibilityElementsHidden?: ?boolean, /** * Whether this `View` should be rendered as a bitmap before compositing. @@ -459,32 +344,107 @@ module.exports = { * * See http://facebook.github.io/react-native/docs/view.html#shouldrasterizeios */ - shouldRasterizeIOS: PropTypes.bool, + shouldRasterizeIOS?: ?boolean, +|}>; + +export type ViewProps = $ReadOnly<{| + ...DirectEventProps, + ...GestureResponderEventProps, + ...TouchEventProps, + ...AndroidViewProps, + ...IOSViewProps, + + // There's no easy way to create a different type if (Platform.isTV): + // so we must include TVViewProps + ...TVViewProps, + + children?: React.Node, + style?: ?ViewStyleProp, /** - * Views that are only used to layout their children or otherwise don't draw - * anything may be automatically removed from the native hierarchy as an - * optimization. Set this property to `false` to disable this optimization and - * ensure that this `View` exists in the native view hierarchy. + * When `true`, indicates that the view is an accessibility element. + * By default, all the touchable elements are accessible. * - * @platform android + * See http://facebook.github.io/react-native/docs/view.html#accessible + */ + accessible?: ?boolean, + + /** + * Overrides the text that's read by the screen reader when the user interacts + * with the element. By default, the label is constructed by traversing all + * the children and accumulating all the `Text` nodes separated by space. * - * See http://facebook.github.io/react-native/docs/view.html#collapsable + * See http://facebook.github.io/react-native/docs/view.html#accessibilitylabel */ - collapsable: PropTypes.bool, + accessibilityLabel?: ?Stringish, /** - * Whether this `View` needs to rendered offscreen and composited with an - * alpha in order to preserve 100% correct colors and blending behavior. + * An accessibility hint helps users understand what will happen when they perform + * an action on the accessibility element when that result is not obvious from the + * accessibility label. * - * @platform android * - * See http://facebook.github.io/react-native/docs/view.html#needsoffscreenalphacompositing + * See http://facebook.github.io/react-native/docs/view.html#accessibilityHint + */ + accessibilityHint?: ?Stringish, + + /** + * Indicates to accessibility services to treat UI component like a specific role. + */ + accessibilityRole?: ?AccessibilityRole, + + /** + * Indicates to accessibility services that UI Component is in a specific State. */ - needsOffscreenAlphaCompositing: PropTypes.bool, + accessibilityStates?: ?AccessibilityStates, /** - * Any additional platform-specific view prop types, or prop type overrides. + * Used to locate this view in end-to-end tests. + * + * > This disables the 'layout-only view removal' optimization for this view! + * + * See http://facebook.github.io/react-native/docs/view.html#testid */ - ...PlatformViewPropTypes, -}; + testID?: ?string, + + /** + * Used to locate this view from native classes. + * + * > This disables the 'layout-only view removal' optimization for this view! + * + * See http://facebook.github.io/react-native/docs/view.html#nativeid + */ + nativeID?: ?string, + + /** + * This defines how far a touch event can start away from the view. + * Typical interface guidelines recommend touch targets that are at least + * 30 - 40 points/density-independent pixels. + * + * > The touch area never extends past the parent view bounds and the Z-index + * > of sibling views always takes precedence if a touch hits two overlapping + * > views. + * + * See http://facebook.github.io/react-native/docs/view.html#hitslop + */ + hitSlop?: ?EdgeInsetsProp, + + /** + * Controls whether the `View` can be the target of touch events. + * + * See http://facebook.github.io/react-native/docs/view.html#pointerevents + */ + pointerEvents?: ?('auto' | 'box-none' | 'box-only' | 'none'), + + /** + * This is a special performance property exposed by `RCTView` and is useful + * for scrolling content when there are many subviews, most of which are + * offscreen. For this property to be effective, it must be applied to a + * view that contains many subviews that extend outside its bound. The + * subviews must also have `overflow: hidden`, as should the containing view + * (or one of its superviews). + * + * See http://facebook.github.io/react-native/docs/view.html#removeclippedsubviews + */ + removeClippedSubviews?: ?boolean, +|}>; diff --git a/Libraries/Components/View/ViewStylePropTypes.js b/Libraries/Components/View/ViewStylePropTypes.js deleted file mode 100644 index 8f690fb32f6891..00000000000000 --- a/Libraries/Components/View/ViewStylePropTypes.js +++ /dev/null @@ -1,61 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * - * 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 ColorPropType = require('ColorPropType'); -const LayoutPropTypes = require('LayoutPropTypes'); -const ReactPropTypes = require('prop-types'); -const ShadowPropTypesIOS = require('ShadowPropTypesIOS'); -const TransformPropTypes = require('TransformPropTypes'); - -/** - * Warning: Some of these properties may not be supported in all releases. - */ -const ViewStylePropTypes = { - ...LayoutPropTypes, - ...ShadowPropTypesIOS, - ...TransformPropTypes, - backfaceVisibility: ReactPropTypes.oneOf(['visible', 'hidden']), - backgroundColor: ColorPropType, - borderColor: ColorPropType, - borderTopColor: ColorPropType, - borderRightColor: ColorPropType, - borderBottomColor: ColorPropType, - borderLeftColor: ColorPropType, - borderStartColor: ColorPropType, - borderEndColor: ColorPropType, - borderRadius: ReactPropTypes.number, - borderTopLeftRadius: ReactPropTypes.number, - borderTopRightRadius: ReactPropTypes.number, - borderTopStartRadius: ReactPropTypes.number, - borderTopEndRadius: ReactPropTypes.number, - borderBottomLeftRadius: ReactPropTypes.number, - borderBottomRightRadius: ReactPropTypes.number, - borderBottomStartRadius: ReactPropTypes.number, - borderBottomEndRadius: ReactPropTypes.number, - borderStyle: ReactPropTypes.oneOf(['solid', 'dotted', 'dashed']), - borderWidth: ReactPropTypes.number, - borderTopWidth: ReactPropTypes.number, - borderRightWidth: ReactPropTypes.number, - borderBottomWidth: ReactPropTypes.number, - borderLeftWidth: ReactPropTypes.number, - opacity: ReactPropTypes.number, - /** - * (Android-only) Sets the elevation of a view, using Android's underlying - * [elevation API](https://developer.android.com/training/material/shadows-clipping.html#Elevation). - * This adds a drop shadow to the item and affects z-order for overlapping views. - * Only supported on Android 5.0+, has no effect on earlier versions. - * @platform android - */ - elevation: ReactPropTypes.number, -}; - -module.exports = ViewStylePropTypes; diff --git a/Libraries/Components/ViewPager/AndroidViewPagerNativeComponent.js b/Libraries/Components/ViewPager/AndroidViewPagerNativeComponent.js new file mode 100644 index 00000000000000..a76092794d144c --- /dev/null +++ b/Libraries/Components/ViewPager/AndroidViewPagerNativeComponent.js @@ -0,0 +1,112 @@ +/** + * 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 requireNativeComponent = require('requireNativeComponent'); + +import type {SyntheticEvent} from 'CoreEventTypes'; +import type {NativeComponent} from 'ReactNative'; +import type {Node} from 'React'; +import type {ViewStyleProp} from 'StyleSheet'; + +type PageScrollState = 'idle' | 'dragging' | 'settling'; + +type PageScrollEvent = SyntheticEvent< + $ReadOnly<{| + position: number, + offset: number, + |}>, +>; + +type PageScrollStateChangedEvent = SyntheticEvent< + $ReadOnly<{| + pageScrollState: PageScrollState, + |}>, +>; + +type PageSelectedEvent = SyntheticEvent< + $ReadOnly<{| + position: number, + |}>, +>; + +type NativeProps = $ReadOnly<{| + /** + * Index of initial page that should be selected. Use `setPage` method to + * update the page, and `onPageSelected` to monitor page changes + */ + initialPage?: ?number, + + /** + * Executed when transitioning between pages (ether because of animation for + * the requested page change or when user is swiping/dragging between pages) + * The `event.nativeEvent` object for this callback will carry following data: + * - position - index of first page from the left that is currently visible + * - offset - value from range [0,1) describing stage between page transitions. + * Value x means that (1 - x) fraction of the page at "position" index is + * visible, and x fraction of the next page is visible. + */ + onPageScroll?: ?(e: PageScrollEvent) => void, + + /** + * Function called when the page scrolling state has changed. + * The page scrolling state can be in 3 states: + * - idle, meaning there is no interaction with the page scroller happening at the time + * - dragging, meaning there is currently an interaction with the page scroller + * - settling, meaning that there was an interaction with the page scroller, and the + * page scroller is now finishing it's closing or opening animation + */ + onPageScrollStateChanged?: ?(e: PageScrollStateChangedEvent) => void, + + /** + * This callback will be called once ViewPager finish navigating to selected page + * (when user swipes between pages). The `event.nativeEvent` object passed to this + * callback will have following fields: + * - position - index of page that has been selected + */ + onPageSelected?: ?(e: PageSelectedEvent) => void, + + /** + * Blank space to show between pages. This is only visible while scrolling, pages are still + * edge-to-edge. + */ + pageMargin?: ?number, + + /** + * Whether enable showing peekFraction or not. If this is true, the preview of + * last and next page will show in current screen. Defaults to false. + */ + + peekEnabled?: ?boolean, + + /** + * Determines whether the keyboard gets dismissed in response to a drag. + * - 'none' (the default), drags do not dismiss the keyboard. + * - 'on-drag', the keyboard is dismissed when a drag begins. + */ + keyboardDismissMode?: ?('none' | 'on-drag'), + + /** + * When false, the content does not scroll. + * The default value is true. + */ + scrollEnabled?: ?boolean, + + children?: Node, + + style?: ?ViewStyleProp, +|}>; + +type ViewPagerNativeType = Class>; + +module.exports = ((requireNativeComponent( + 'AndroidViewPager', +): any): ViewPagerNativeType); diff --git a/Libraries/Components/ViewPager/ViewPagerAndroid.android.js b/Libraries/Components/ViewPager/ViewPagerAndroid.android.js index a3ca76742aab5a..0c4b2ad41ac5d3 100644 --- a/Libraries/Components/ViewPager/ViewPagerAndroid.android.js +++ b/Libraries/Components/ViewPager/ViewPagerAndroid.android.js @@ -1,29 +1,48 @@ /** - * Copyright (c) 2013-present, Facebook, Inc. + * 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 + * @flow strict-local */ 'use strict'; const React = require('React'); -const PropTypes = require('prop-types'); const ReactNative = require('ReactNative'); const UIManager = require('UIManager'); -const ViewPropTypes = require('ViewPropTypes'); const dismissKeyboard = require('dismissKeyboard'); -const requireNativeComponent = require('requireNativeComponent'); -const NativeAndroidViewPager = requireNativeComponent('AndroidViewPager'); +const NativeAndroidViewPager = require('AndroidViewPagerNativeComponent'); + +import type {SyntheticEvent} from 'CoreEventTypes'; +import type {ViewStyleProp} from 'StyleSheet'; const VIEWPAGER_REF = 'viewPager'; -type Event = Object; +type PageScrollState = 'idle' | 'dragging' | 'settling'; + +type PageScrollEvent = SyntheticEvent< + $ReadOnly<{| + position: number, + offset: number, + |}>, +>; + +type PageScrollStateChangedEvent = SyntheticEvent< + $ReadOnly<{| + pageScrollState: PageScrollState, + |}>, +>; + +type PageSelectedEvent = SyntheticEvent< + $ReadOnly<{| + position: number, + |}>, +>; export type ViewPagerScrollState = $Enum<{ idle: string, @@ -31,6 +50,73 @@ export type ViewPagerScrollState = $Enum<{ settling: string, }>; +type Props = $ReadOnly<{| + /** + * Index of initial page that should be selected. Use `setPage` method to + * update the page, and `onPageSelected` to monitor page changes + */ + initialPage?: ?number, + + /** + * Executed when transitioning between pages (ether because of animation for + * the requested page change or when user is swiping/dragging between pages) + * The `event.nativeEvent` object for this callback will carry following data: + * - position - index of first page from the left that is currently visible + * - offset - value from range [0,1) describing stage between page transitions. + * Value x means that (1 - x) fraction of the page at "position" index is + * visible, and x fraction of the next page is visible. + */ + onPageScroll?: ?(e: PageScrollEvent) => void, + + /** + * Function called when the page scrolling state has changed. + * The page scrolling state can be in 3 states: + * - idle, meaning there is no interaction with the page scroller happening at the time + * - dragging, meaning there is currently an interaction with the page scroller + * - settling, meaning that there was an interaction with the page scroller, and the + * page scroller is now finishing it's closing or opening animation + */ + onPageScrollStateChanged?: ?(e: PageScrollStateChangedEvent) => void, + + /** + * This callback will be called once ViewPager finish navigating to selected page + * (when user swipes between pages). The `event.nativeEvent` object passed to this + * callback will have following fields: + * - position - index of page that has been selected + */ + onPageSelected?: ?(e: PageSelectedEvent) => void, + + /** + * Blank space to show between pages. This is only visible while scrolling, pages are still + * edge-to-edge. + */ + pageMargin?: ?number, + + /** + * Whether enable showing peekFraction or not. If this is true, the preview of + * last and next page will show in current screen. Defaults to false. + */ + + peekEnabled?: ?boolean, + + /** + * Determines whether the keyboard gets dismissed in response to a drag. + * - 'none' (the default), drags do not dismiss the keyboard. + * - 'on-drag', the keyboard is dismissed when a drag begins. + */ + keyboardDismissMode?: ?('none' | 'on-drag'), + + /** + * When false, the content does not scroll. + * The default value is true. + */ + scrollEnabled?: ?boolean, + + children?: React.Node, + + style?: ?ViewStyleProp, +|}>; + /** * Container that allows to flip left and right between child views. Each * child view of the `ViewPagerAndroid` will be treated as a separate page @@ -72,92 +158,22 @@ export type ViewPagerScrollState = $Enum<{ * } * ``` */ -class ViewPagerAndroid extends React.Component<{ - initialPage?: number, - onPageScroll?: Function, - onPageScrollStateChanged?: Function, - onPageSelected?: Function, - pageMargin?: number, - peekEnabled?: boolean, - keyboardDismissMode?: 'none' | 'on-drag', - scrollEnabled?: boolean, -}> { - static propTypes = { - ...ViewPropTypes, - /** - * Index of initial page that should be selected. Use `setPage` method to - * update the page, and `onPageSelected` to monitor page changes - */ - initialPage: PropTypes.number, - - /** - * Executed when transitioning between pages (ether because of animation for - * the requested page change or when user is swiping/dragging between pages) - * The `event.nativeEvent` object for this callback will carry following data: - * - position - index of first page from the left that is currently visible - * - offset - value from range [0,1) describing stage between page transitions. - * Value x means that (1 - x) fraction of the page at "position" index is - * visible, and x fraction of the next page is visible. - */ - onPageScroll: PropTypes.func, - - /** - * Function called when the page scrolling state has changed. - * The page scrolling state can be in 3 states: - * - idle, meaning there is no interaction with the page scroller happening at the time - * - dragging, meaning there is currently an interaction with the page scroller - * - settling, meaning that there was an interaction with the page scroller, and the - * page scroller is now finishing it's closing or opening animation - */ - onPageScrollStateChanged: PropTypes.func, - - /** - * This callback will be called once ViewPager finish navigating to selected page - * (when user swipes between pages). The `event.nativeEvent` object passed to this - * callback will have following fields: - * - position - index of page that has been selected - */ - onPageSelected: PropTypes.func, - - /** - * Blank space to show between pages. This is only visible while scrolling, pages are still - * edge-to-edge. - */ - pageMargin: PropTypes.number, - - /** - * Determines whether the keyboard gets dismissed in response to a drag. - * - 'none' (the default), drags do not dismiss the keyboard. - * - 'on-drag', the keyboard is dismissed when a drag begins. - */ - keyboardDismissMode: PropTypes.oneOf([ - 'none', // default - 'on-drag', - ]), - - /** - * When false, the content does not scroll. - * The default value is true. - */ - scrollEnabled: PropTypes.bool, - - /** - * Whether enable showing peekFraction or not. If this is true, the preview of - * last and next page will show in current screen. Defaults to false. - */ - peekEnabled: PropTypes.bool, - }; +class ViewPagerAndroid extends React.Component { componentDidMount() { if (this.props.initialPage != null) { this.setPageWithoutAnimation(this.props.initialPage); } } + /* $FlowFixMe(>=0.78.0 site=react_native_android_fb) This issue was found + * when making Flow check .android.js files. */ getInnerViewNode = (): ReactComponent => { return this.refs[VIEWPAGER_REF].getInnerViewNode(); }; + /* $FlowFixMe(>=0.78.0 site=react_native_android_fb) This issue was found + * when making Flow check .android.js files. */ _childrenWithOverridenStyle = (): Array => { // Override styles so that each page will fill the parent. Native component // will handle positioning of elements, so it's not important to offset @@ -197,7 +213,7 @@ class ViewPagerAndroid extends React.Component<{ }); }; - _onPageScroll = (e: Event) => { + _onPageScroll = (e: PageScrollEvent) => { if (this.props.onPageScroll) { this.props.onPageScroll(e); } @@ -206,13 +222,13 @@ class ViewPagerAndroid extends React.Component<{ } }; - _onPageScrollStateChanged = (e: Event) => { + _onPageScrollStateChanged = (e: PageScrollStateChangedEvent) => { if (this.props.onPageScrollStateChanged) { - this.props.onPageScrollStateChanged(e.nativeEvent.pageScrollState); + this.props.onPageScrollStateChanged(e); } }; - _onPageSelected = (e: Event) => { + _onPageSelected = (e: PageSelectedEvent) => { if (this.props.onPageSelected) { this.props.onPageSelected(e); } @@ -225,7 +241,7 @@ class ViewPagerAndroid extends React.Component<{ setPage = (selectedPage: number) => { UIManager.dispatchViewManagerCommand( ReactNative.findNodeHandle(this), - UIManager.AndroidViewPager.Commands.setPage, + UIManager.getViewManagerConfig('AndroidViewPager').Commands.setPage, [selectedPage], ); }; @@ -237,7 +253,8 @@ class ViewPagerAndroid extends React.Component<{ setPageWithoutAnimation = (selectedPage: number) => { UIManager.dispatchViewManagerCommand( ReactNative.findNodeHandle(this), - UIManager.AndroidViewPager.Commands.setPageWithoutAnimation, + UIManager.getViewManagerConfig('AndroidViewPager').Commands + .setPageWithoutAnimation, [selectedPage], ); }; diff --git a/Libraries/Components/ViewPager/ViewPagerAndroid.ios.js b/Libraries/Components/ViewPager/ViewPagerAndroid.ios.js index 260d559929796f..7a00b6636cf664 100644 --- a/Libraries/Components/ViewPager/ViewPagerAndroid.ios.js +++ b/Libraries/Components/ViewPager/ViewPagerAndroid.ios.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/Components/WebView/WebView.android.js b/Libraries/Components/WebView/WebView.android.js index 4d4b65fb11fdc8..d47a6d6c695b69 100644 --- a/Libraries/Components/WebView/WebView.android.js +++ b/Libraries/Components/WebView/WebView.android.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -9,15 +9,15 @@ 'use strict'; -const EdgeInsetsPropType = require('EdgeInsetsPropType'); const ActivityIndicator = require('ActivityIndicator'); -const React = require('React'); +const DeprecatedViewPropTypes = require('DeprecatedViewPropTypes'); +const DeprecatedEdgeInsetsPropType = require('DeprecatedEdgeInsetsPropType'); const PropTypes = require('prop-types'); +const React = require('React'); const ReactNative = require('ReactNative'); const StyleSheet = require('StyleSheet'); const UIManager = require('UIManager'); const View = require('View'); -const ViewPropTypes = require('ViewPropTypes'); const WebViewShared = require('WebViewShared'); const deprecatedPropType = require('deprecatedPropType'); @@ -44,7 +44,7 @@ const defaultRenderLoading = () => ( */ class WebView extends React.Component { static propTypes = { - ...ViewPropTypes, + ...DeprecatedViewPropTypes, renderError: PropTypes.func, renderLoading: PropTypes.func, onLoad: PropTypes.func, @@ -52,12 +52,12 @@ class WebView extends React.Component { onLoadStart: PropTypes.func, onError: PropTypes.func, automaticallyAdjustContentInsets: PropTypes.bool, - contentInset: EdgeInsetsPropType, + contentInset: DeprecatedEdgeInsetsPropType, onNavigationStateChange: PropTypes.func, onMessage: PropTypes.func, onContentSizeChange: PropTypes.func, startInLoadingState: PropTypes.bool, // force WebView to show loadingView on first load - style: ViewPropTypes.style, + style: DeprecatedViewPropTypes.style, html: deprecatedPropType( PropTypes.string, @@ -109,6 +109,22 @@ class WebView extends React.Component { PropTypes.number, ]), + /** + * If true, use WKWebView instead of UIWebView. + * @platform ios + */ + useWebKit: PropTypes.bool, + + /** + * Used on Android only to disable Hardware Acceleration if needed + * Hardware acceleration can not be enabled at view level but it can be + * disabled see: + * https://developer.android.com/guide/topics/graphics/hardware-accel + * + * @platform android + */ + hardwareAccelerationEnabledExperimental: PropTypes.bool, + /** * Used on Android only, JS is enabled by default for WebView on iOS * @platform android @@ -144,6 +160,12 @@ class WebView extends React.Component { */ scalesPageToFit: PropTypes.bool, + /** + * Sets whether the webview allow access to file system. + * @platform android + */ + allowFileAccess: PropTypes.bool, + /** * Sets the user-agent for this WebView. The user-agent can also be set in native using * WebViewConfig. This prop will overwrite that config. @@ -236,6 +258,7 @@ class WebView extends React.Component { javaScriptEnabled: true, thirdPartyCookiesEnabled: true, scalesPageToFit: true, + hardwareAccelerationEnabledExperimental: true, saveFormDataDisabled: false, originWhitelist: WebViewShared.defaultOriginWhitelist, }; @@ -311,9 +334,13 @@ class WebView extends React.Component { style={webViewStyles} source={resolveAssetSource(source)} scalesPageToFit={this.props.scalesPageToFit} + allowFileAccess={this.props.allowFileAccess} injectedJavaScript={this.props.injectedJavaScript} userAgent={this.props.userAgent} javaScriptEnabled={this.props.javaScriptEnabled} + hardwareAccelerationEnabledExperimental={ + this.props.hardwareAccelerationEnabledExperimental + } thirdPartyCookiesEnabled={this.props.thirdPartyCookiesEnabled} domStorageEnabled={this.props.domStorageEnabled} messagingEnabled={typeof this.props.onMessage === 'function'} @@ -353,7 +380,7 @@ class WebView extends React.Component { goForward = () => { UIManager.dispatchViewManagerCommand( this.getWebViewHandle(), - UIManager.RCTWebView.Commands.goForward, + UIManager.getViewManagerConfig('RCTWebView').Commands.goForward, null, ); }; @@ -361,7 +388,7 @@ class WebView extends React.Component { goBack = () => { UIManager.dispatchViewManagerCommand( this.getWebViewHandle(), - UIManager.RCTWebView.Commands.goBack, + UIManager.getViewManagerConfig('RCTWebView').Commands.goBack, null, ); }; @@ -372,7 +399,7 @@ class WebView extends React.Component { }); UIManager.dispatchViewManagerCommand( this.getWebViewHandle(), - UIManager.RCTWebView.Commands.reload, + UIManager.getViewManagerConfig('RCTWebView').Commands.reload, null, ); }; @@ -380,7 +407,7 @@ class WebView extends React.Component { stopLoading = () => { UIManager.dispatchViewManagerCommand( this.getWebViewHandle(), - UIManager.RCTWebView.Commands.stopLoading, + UIManager.getViewManagerConfig('RCTWebView').Commands.stopLoading, null, ); }; @@ -388,7 +415,7 @@ class WebView extends React.Component { postMessage = data => { UIManager.dispatchViewManagerCommand( this.getWebViewHandle(), - UIManager.RCTWebView.Commands.postMessage, + UIManager.getViewManagerConfig('RCTWebView').Commands.postMessage, [String(data)], ); }; @@ -402,7 +429,7 @@ class WebView extends React.Component { injectJavaScript = data => { UIManager.dispatchViewManagerCommand( this.getWebViewHandle(), - UIManager.RCTWebView.Commands.injectJavaScript, + UIManager.getViewManagerConfig('RCTWebView').Commands.injectJavaScript, [data], ); }; diff --git a/Libraries/Components/WebView/WebView.ios.js b/Libraries/Components/WebView/WebView.ios.js index 56290df97bcf94..cb6c8b0a08c1ba 100644 --- a/Libraries/Components/WebView/WebView.ios.js +++ b/Libraries/Components/WebView/WebView.ios.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -11,27 +11,27 @@ 'use strict'; const ActivityIndicator = require('ActivityIndicator'); -const EdgeInsetsPropType = require('EdgeInsetsPropType'); +const DeprecatedViewPropTypes = require('DeprecatedViewPropTypes'); +const DeprecatedEdgeInsetsPropType = require('DeprecatedEdgeInsetsPropType'); const Linking = require('Linking'); const PropTypes = require('prop-types'); const React = require('React'); const ReactNative = require('ReactNative'); -const ScrollView = require('ScrollView'); const StyleSheet = require('StyleSheet'); const Text = require('Text'); const UIManager = require('UIManager'); const View = require('View'); -const ViewPropTypes = require('ViewPropTypes'); const WebViewShared = require('WebViewShared'); const deprecatedPropType = require('deprecatedPropType'); -const invariant = require('fbjs/lib/invariant'); +const invariant = require('invariant'); const keyMirror = require('fbjs/lib/keyMirror'); const processDecelerationRate = require('processDecelerationRate'); const requireNativeComponent = require('requireNativeComponent'); const resolveAssetSource = require('resolveAssetSource'); const RCTWebViewManager = require('NativeModules').WebViewManager; +const RCTWKWebViewManager = require('NativeModules').WKWebViewManager; const BGWASH = 'rgba(255,255,255,0.8)'; const RCT_WEBVIEW_REF = 'webview'; @@ -66,6 +66,9 @@ const DataDetectorTypes = [ 'link', 'address', 'calendarEvent', + 'trackingNumber', + 'flightNumber', + 'lookupSuggestion', 'none', 'all', ]; @@ -110,7 +113,7 @@ class WebView extends React.Component { static JSNavigationScheme = JSNavigationScheme; static NavigationType = NavigationType; static propTypes = { - ...ViewPropTypes, + ...DeprecatedViewPropTypes, html: deprecatedPropType( PropTypes.string, @@ -162,6 +165,12 @@ class WebView extends React.Component { PropTypes.number, ]), + /** + * If true, use WKWebView instead of UIWebView. + * @platform ios + */ + useWebKit: PropTypes.bool, + /** * Function that returns a view to show if there's an error. */ @@ -224,7 +233,7 @@ class WebView extends React.Component { * the scroll view. Defaults to {top: 0, left: 0, bottom: 0, right: 0}. * @platform ios */ - contentInset: EdgeInsetsPropType, + contentInset: DeprecatedEdgeInsetsPropType, /** * Function that is invoked when the `WebView` loading starts or ends. */ @@ -247,7 +256,7 @@ class WebView extends React.Component { /** * The style to apply to the `WebView`. */ - style: ViewPropTypes.style, + style: DeprecatedViewPropTypes.style, /** * Determines the types of data converted to clickable URLs in the web view's content. @@ -264,6 +273,11 @@ class WebView extends React.Component { * - `'none'` * - `'all'` * + * With the new WebKit implementation, we have three new values: + * - `'trackingNumber'`, + * - `'flightNumber'`, + * - `'lookupSuggestion'`, + * * @platform ios */ dataDetectorTypes: PropTypes.oneOfType([ @@ -271,6 +285,16 @@ class WebView extends React.Component { PropTypes.arrayOf(PropTypes.oneOf(DataDetectorTypes)), ]), + /** + * Used on Android only to disable Hardware Acceleration if needed + * Hardware acceleration can not be enabled at view level but it can be + * disabled see: + * https://developer.android.com/guide/topics/graphics/hardware-accel + * + * @platform android + */ + hardwareAccelerationEnabledExperimental: PropTypes.bool, + /** * Boolean value to enable JavaScript in the `WebView`. Used on Android only * as JavaScript is enabled by default on iOS. The default value is `true`. @@ -309,6 +333,8 @@ class WebView extends React.Component { * Boolean that controls whether the web content is scaled to fit * the view and enables the user to change the scale. The default value * is `true`. + * + * On iOS, when `useWebKit=true`, this prop will not work. */ scalesPageToFit: PropTypes.bool, @@ -388,7 +414,6 @@ class WebView extends React.Component { static defaultProps = { originWhitelist: WebViewShared.defaultOriginWhitelist, - scalesPageToFit: true, }; state = { @@ -401,11 +426,28 @@ class WebView extends React.Component { if (this.props.startInLoadingState) { this.setState({viewState: WebViewState.LOADING}); } + + if ( + this.props.useWebKit === true && + this.props.scalesPageToFit !== undefined + ) { + console.warn( + 'The scalesPageToFit property is not supported when useWebKit = true', + ); + } } render() { let otherView = null; + let scalesPageToFit; + + if (this.props.useWebKit) { + ({scalesPageToFit} = this.props); + } else { + ({scalesPageToFit = true} = this.props); + } + if (this.state.viewState === WebViewState.LOADING) { otherView = (this.props.renderLoading || defaultRenderLoading)(); } else if (this.state.viewState === WebViewState.ERROR) { @@ -433,11 +475,18 @@ class WebView extends React.Component { const nativeConfig = this.props.nativeConfig || {}; - const viewManager = nativeConfig.viewManager || RCTWebViewManager; + let viewManager = nativeConfig.viewManager; - const compiledWhitelist = (this.props.originWhitelist || []).map( - WebViewShared.originWhitelistToRegex, - ); + if (this.props.useWebKit) { + viewManager = viewManager || RCTWKWebViewManager; + } else { + viewManager = viewManager || RCTWebViewManager; + } + + const compiledWhitelist = [ + 'about:blank', + ...(this.props.originWhitelist || []), + ].map(WebViewShared.originWhitelistToRegex); const onShouldStartLoadWithRequest = (event: Event) => { let shouldStart = true; const {url} = event.nativeEvent; @@ -473,7 +522,13 @@ class WebView extends React.Component { const messagingEnabled = typeof this.props.onMessage === 'function'; - const NativeWebView = nativeConfig.component || RCTWebView; + let NativeWebView = nativeConfig.component; + + if (this.props.useWebKit) { + NativeWebView = NativeWebView || RCTWKWebView; + } else { + NativeWebView = NativeWebView || RCTWebView; + } const webView = ( { UIManager.dispatchViewManagerCommand( this.getWebViewHandle(), - UIManager.RCTWebView.Commands.goForward, + this._getCommands().goForward, null, ); }; @@ -530,7 +593,7 @@ class WebView extends React.Component { goBack = () => { UIManager.dispatchViewManagerCommand( this.getWebViewHandle(), - UIManager.RCTWebView.Commands.goBack, + this._getCommands().goBack, null, ); }; @@ -542,7 +605,7 @@ class WebView extends React.Component { this.setState({viewState: WebViewState.LOADING}); UIManager.dispatchViewManagerCommand( this.getWebViewHandle(), - UIManager.RCTWebView.Commands.reload, + this._getCommands().reload, null, ); }; @@ -553,7 +616,7 @@ class WebView extends React.Component { stopLoading = () => { UIManager.dispatchViewManagerCommand( this.getWebViewHandle(), - UIManager.RCTWebView.Commands.stopLoading, + this._getCommands().stopLoading, null, ); }; @@ -571,7 +634,7 @@ class WebView extends React.Component { postMessage = data => { UIManager.dispatchViewManagerCommand( this.getWebViewHandle(), - UIManager.RCTWebView.Commands.postMessage, + this._getCommands().postMessage, [String(data)], ); }; @@ -585,7 +648,7 @@ class WebView extends React.Component { injectJavaScript = data => { UIManager.dispatchViewManagerCommand( this.getWebViewHandle(), - UIManager.RCTWebView.Commands.injectJavaScript, + this._getCommands().injectJavaScript, [data], ); }; @@ -640,9 +703,42 @@ class WebView extends React.Component { const {onMessage} = this.props; onMessage && onMessage(event); }; + + componentDidUpdate(prevProps) { + if (!(prevProps.useWebKit && this.props.useWebKit)) { + return; + } + + this._showRedboxOnPropChanges(prevProps, 'allowsInlineMediaPlayback'); + this._showRedboxOnPropChanges(prevProps, 'mediaPlaybackRequiresUserAction'); + this._showRedboxOnPropChanges(prevProps, 'dataDetectorTypes'); + + if (this.props.scalesPageToFit !== undefined) { + console.warn( + 'The scalesPageToFit property is not supported when useWebKit = true', + ); + } + } + + _showRedboxOnPropChanges(prevProps, propName: string) { + if (this.props[propName] !== prevProps[propName]) { + console.error( + `Changes to property ${propName} do nothing after the initial render.`, + ); + } + } } -const RCTWebView = requireNativeComponent('RCTWebView'); +const RCTWebView = requireNativeComponent( + 'RCTWebView', + WebView, + WebView.extraNativeComponentConfig, +); +const RCTWKWebView = requireNativeComponent( + 'RCTWKWebView', + WebView, + WebView.extraNativeComponentConfig, +); const styles = StyleSheet.create({ container: { diff --git a/Libraries/Components/WebView/WebViewShared.js b/Libraries/Components/WebView/WebViewShared.js index 5a3006e845b0cc..af2f7e9a80baaa 100644 --- a/Libraries/Components/WebView/WebViewShared.js +++ b/Libraries/Components/WebView/WebViewShared.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/Components/WebView/__tests__/WebViewShared-test.js b/Libraries/Components/WebView/__tests__/WebViewShared-test.js index b7456638fedb71..2a6dee2e2758de 100644 --- a/Libraries/Components/WebView/__tests__/WebViewShared-test.js +++ b/Libraries/Components/WebView/__tests__/WebViewShared-test.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2013-present, Facebook, Inc. + * 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. diff --git a/Libraries/Core/Devtools/__tests__/parseErrorStack-test.js b/Libraries/Core/Devtools/__tests__/parseErrorStack-test.js index 79d677e217e71c..14aafede381848 100644 --- a/Libraries/Core/Devtools/__tests__/parseErrorStack-test.js +++ b/Libraries/Core/Devtools/__tests__/parseErrorStack-test.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/Core/Devtools/getDevServer.js b/Libraries/Core/Devtools/getDevServer.js index adcc28c2d067e1..3eae2fbc404baf 100644 --- a/Libraries/Core/Devtools/getDevServer.js +++ b/Libraries/Core/Devtools/getDevServer.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/Core/Devtools/openFileInEditor.js b/Libraries/Core/Devtools/openFileInEditor.js index c085883920aeda..00074b91eefedf 100644 --- a/Libraries/Core/Devtools/openFileInEditor.js +++ b/Libraries/Core/Devtools/openFileInEditor.js @@ -1,11 +1,11 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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 + * @flow strict-local */ 'use strict'; diff --git a/Libraries/Core/Devtools/parseErrorStack.js b/Libraries/Core/Devtools/parseErrorStack.js index ec85260d7eaa35..ff308d1dd243cf 100644 --- a/Libraries/Core/Devtools/parseErrorStack.js +++ b/Libraries/Core/Devtools/parseErrorStack.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -19,6 +19,7 @@ export type StackFrame = { export type ExtendedError = Error & { framesToPop?: number, + jsEngine?: string, }; function parseErrorStack(e: ExtendedError): Array { diff --git a/Libraries/Core/Devtools/setupDevtools.js b/Libraries/Core/Devtools/setupDevtools.js index ddca7d04003025..35b2a6ba50fb25 100644 --- a/Libraries/Core/Devtools/setupDevtools.js +++ b/Libraries/Core/Devtools/setupDevtools.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -10,16 +10,6 @@ 'use strict'; -type DevToolsPluginConnection = { - isAppActive: () => boolean, - host: string, - port: number, -}; - -type DevToolsPlugin = { - connectToDevTools: (connection: DevToolsPluginConnection) => void, -}; - let register = function() { // noop }; diff --git a/Libraries/Core/Devtools/symbolicateStackTrace.js b/Libraries/Core/Devtools/symbolicateStackTrace.js index b5d7d4e9754ff0..93d643cdb3380d 100644 --- a/Libraries/Core/Devtools/symbolicateStackTrace.js +++ b/Libraries/Core/Devtools/symbolicateStackTrace.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/Core/ExceptionsManager.js b/Libraries/Core/ExceptionsManager.js index bc32f5d0d23890..08fe06f72b6c47 100644 --- a/Libraries/Core/ExceptionsManager.js +++ b/Libraries/Core/ExceptionsManager.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -22,18 +22,16 @@ function reportException(e: ExtendedError, isFatal: boolean) { const parseErrorStack = require('parseErrorStack'); const stack = parseErrorStack(e); const currentExceptionID = ++exceptionID; + const message = + e.jsEngine == null ? e.message : `${e.message}, js engine: ${e.jsEngine}`; if (isFatal) { ExceptionsManager.reportFatalException( - e.message, + message, stack, currentExceptionID, ); } else { - ExceptionsManager.reportSoftException( - e.message, - stack, - currentExceptionID, - ); + ExceptionsManager.reportSoftException(message, stack, currentExceptionID); } if (__DEV__) { const symbolicateStackTrace = require('symbolicateStackTrace'); diff --git a/Libraries/Core/InitializeCore.js b/Libraries/Core/InitializeCore.js index 119deb92b2b0bc..2a2ca6479b5399 100644 --- a/Libraries/Core/InitializeCore.js +++ b/Libraries/Core/InitializeCore.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -25,198 +25,31 @@ */ 'use strict'; -const {polyfillObjectProperty, polyfillGlobal} = require('PolyfillFunctions'); - -if (global.GLOBAL === undefined) { - global.GLOBAL = global; -} - -if (global.window === undefined) { - global.window = global; -} - -// Set up collections -const _shouldPolyfillCollection = require('_shouldPolyfillES6Collection'); -if (_shouldPolyfillCollection('Map')) { - polyfillGlobal('Map', () => require('Map')); -} -if (_shouldPolyfillCollection('Set')) { - polyfillGlobal('Set', () => require('Set')); -} - -// Set up process -global.process = global.process || {}; -global.process.env = global.process.env || {}; -if (!global.process.env.NODE_ENV) { - global.process.env.NODE_ENV = __DEV__ ? 'development' : 'production'; -} - -// Setup the Systrace profiling hooks if necessary -if (global.__RCTProfileIsProfiling) { - const Systrace = require('Systrace'); - Systrace.installReactHook(); - Systrace.setEnabled(true); -} - -// Set up console -const ExceptionsManager = require('ExceptionsManager'); -ExceptionsManager.installConsoleErrorReporter(); - -// Set up error handler -if (!global.__fbDisableExceptionsManager) { - const handleError = (e, isFatal) => { - try { - ExceptionsManager.handleException(e, isFatal); - } catch (ee) { - console.log('Failed to print error: ', ee.message); - throw e; - } - }; - - const ErrorUtils = require('ErrorUtils'); - ErrorUtils.setGlobalHandler(handleError); -} - -// Check for compatibility between the JS and native code -const ReactNativeVersionCheck = require('ReactNativeVersionCheck'); -ReactNativeVersionCheck.checkVersions(); - -// Set up Promise -// The native Promise implementation throws the following error: -// ERROR: Event loop not supported. -polyfillGlobal('Promise', () => require('Promise')); - -// Set up regenerator. -polyfillGlobal('regeneratorRuntime', () => { - // The require just sets up the global, so make sure when we first - // invoke it the global does not exist - delete global.regeneratorRuntime; - /* $FlowFixMe(>=0.54.0 site=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. */ - require('regenerator-runtime/runtime'); - return global.regeneratorRuntime; -}); - -// Set up timers -const defineLazyTimer = name => { - polyfillGlobal(name, () => require('JSTimers')[name]); -}; -defineLazyTimer('setTimeout'); -defineLazyTimer('setInterval'); -defineLazyTimer('setImmediate'); -defineLazyTimer('clearTimeout'); -defineLazyTimer('clearInterval'); -defineLazyTimer('clearImmediate'); -defineLazyTimer('requestAnimationFrame'); -defineLazyTimer('cancelAnimationFrame'); -defineLazyTimer('requestIdleCallback'); -defineLazyTimer('cancelIdleCallback'); - -// Set up XHR -// The native XMLHttpRequest in Chrome dev tools is CORS aware and won't -// let you fetch anything from the internet -polyfillGlobal('XMLHttpRequest', () => require('XMLHttpRequest')); -polyfillGlobal('FormData', () => require('FormData')); - -polyfillGlobal('fetch', () => require('fetch').fetch); -polyfillGlobal('Headers', () => require('fetch').Headers); -polyfillGlobal('Request', () => require('fetch').Request); -polyfillGlobal('Response', () => require('fetch').Response); -polyfillGlobal('WebSocket', () => require('WebSocket')); -polyfillGlobal('Blob', () => require('Blob')); -polyfillGlobal('File', () => require('File')); -polyfillGlobal('FileReader', () => require('FileReader')); -polyfillGlobal('URL', () => require('URL')); - -// Set up alert -if (!global.alert) { - global.alert = function(text) { - // Require Alert on demand. Requiring it too early can lead to issues - // with things like Platform not being fully initialized. - require('Alert').alert('Alert', '' + text); - }; -} - -// Set up Geolocation -let navigator = global.navigator; -if (navigator === undefined) { - global.navigator = navigator = {}; +const start = Date.now(); + +require('setUpGlobals'); +require('polyfillES6Collections'); +require('setUpSystrace'); +require('setUpErrorHandling'); +require('checkNativeVersion'); +require('polyfillPromise'); +require('setUpRegeneratorRuntime'); +require('setUpTimers'); +require('setUpXHR'); +require('setUpAlert'); +require('setUpGeolocation'); +require('setUpBatchedBridge'); +require('setUpSegmentFetcher'); +if (__DEV__) { + require('setUpDeveloperTools'); } -// see https://github.com/facebook/react-native/issues/10881 -polyfillObjectProperty(navigator, 'product', () => 'ReactNative'); -polyfillObjectProperty(navigator, 'geolocation', () => require('Geolocation')); - -// Just to make sure the JS gets packaged up. Wait until the JS environment has -// been initialized before requiring them. -const BatchedBridge = require('BatchedBridge'); -BatchedBridge.registerLazyCallableModule('Systrace', () => require('Systrace')); -BatchedBridge.registerLazyCallableModule('JSTimers', () => require('JSTimers')); -BatchedBridge.registerLazyCallableModule('HeapCapture', () => - require('HeapCapture'), -); -BatchedBridge.registerLazyCallableModule('SamplingProfiler', () => - require('SamplingProfiler'), -); -BatchedBridge.registerLazyCallableModule('RCTLog', () => require('RCTLog')); -BatchedBridge.registerLazyCallableModule('RCTDeviceEventEmitter', () => - require('RCTDeviceEventEmitter'), -); -BatchedBridge.registerLazyCallableModule('RCTNativeAppEventEmitter', () => - require('RCTNativeAppEventEmitter'), -); -BatchedBridge.registerLazyCallableModule('PerformanceLogger', () => - require('PerformanceLogger'), +const PerformanceLogger = require('PerformanceLogger'); +// We could just call PerformanceLogger.markPoint at the top of the file, +// but then we'd be excluding the time it took to require PerformanceLogger. +// Instead, we just use Date.now and backdate the timestamp. +PerformanceLogger.markPoint( + 'initializeCore_start', + PerformanceLogger.currentTimestamp() - (Date.now() - start), ); -BatchedBridge.registerLazyCallableModule('JSDevSupportModule', () => - require('JSDevSupportModule'), -); - -global.__fetchSegment = function( - segmentId: number, - options: {|+otaBuildNumber: ?string|}, - callback: (?Error) => void, -) { - const {SegmentFetcher} = require('NativeModules'); - if (!SegmentFetcher) { - throw new Error( - 'SegmentFetcher is missing. Please ensure that it is ' + - 'included as a NativeModule.', - ); - } - - SegmentFetcher.fetchSegment( - segmentId, - options, - (errorObject: ?{message: string, code: string}) => { - if (errorObject) { - const error = new Error(errorObject.message); - (error: any).code = errorObject.code; - callback(error); - } - - callback(null); - }, - ); -}; - -// Set up devtools -if (__DEV__) { - if (!global.__RCTProfileIsProfiling) { - BatchedBridge.registerCallableModule('HMRClient', require('HMRClient')); - - // not when debugging in chrome - // TODO(t12832058) This check is broken - if (!window.document) { - require('setupDevtools'); - } - - // Set up inspector - const JSInspector = require('JSInspector'); - /* $FlowFixMe(>=0.56.0 site=react_native_fb,react_native_oss) This comment - * suppresses an error found when Flow v0.56 was deployed. To see the error - * delete this comment and run Flow. */ - JSInspector.registerAgent(require('NetworkAgent')); - } -} +PerformanceLogger.markPoint('initializeCore_end'); diff --git a/Libraries/Core/ReactNativeVersion.js b/Libraries/Core/ReactNativeVersion.js index 320b26ac84db6b..1fcb2e9eb61be9 100644 --- a/Libraries/Core/ReactNativeVersion.js +++ b/Libraries/Core/ReactNativeVersion.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/Core/ReactNativeVersionCheck.js b/Libraries/Core/ReactNativeVersionCheck.js index 8a937c769d9979..578b205645a487 100644 --- a/Libraries/Core/ReactNativeVersionCheck.js +++ b/Libraries/Core/ReactNativeVersionCheck.js @@ -1,10 +1,10 @@ /** - * Copyright (c) 2017-present, Facebook, Inc. + * 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 + * @flow strict-local * @format */ 'use strict'; diff --git a/Libraries/Core/Timers/JSTimers.js b/Libraries/Core/Timers/JSTimers.js index 7cffb4e3d06d82..d54fab92d5cc84 100644 --- a/Libraries/Core/Timers/JSTimers.js +++ b/Libraries/Core/Timers/JSTimers.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -12,7 +12,7 @@ const Platform = require('Platform'); const Systrace = require('Systrace'); -const invariant = require('fbjs/lib/invariant'); +const invariant = require('invariant'); const {Timing} = require('NativeModules'); const BatchedBridge = require('BatchedBridge'); @@ -290,6 +290,9 @@ const JSTimers = { * @param {function} func Callback to be invoked before the end of the * current JavaScript execution loop. */ + /* $FlowFixMe(>=0.79.1 site=react_native_fb) This comment suppresses an + * error found when Flow v0.79 was deployed. To see the error delete this + * comment and run Flow. */ setImmediate: function(func: Function, ...args: any) { const id = _allocateCallback( () => func.apply(undefined, args), @@ -302,6 +305,9 @@ const JSTimers = { /** * @param {function} func Callback to be invoked every frame. */ + /* $FlowFixMe(>=0.79.1 site=react_native_fb) This comment suppresses an + * error found when Flow v0.79 was deployed. To see the error delete this + * comment and run Flow. */ requestAnimationFrame: function(func: Function) { const id = _allocateCallback(func, 'requestAnimationFrame'); Timing.createTimer(id, 1, Date.now(), /* recurring */ false); @@ -313,6 +319,9 @@ const JSTimers = { * with time remaining in frame. * @param {?object} options */ + /* $FlowFixMe(>=0.79.1 site=react_native_fb) This comment suppresses an + * error found when Flow v0.79 was deployed. To see the error delete this + * comment and run Flow. */ requestIdleCallback: function(func: Function, options: ?Object) { if (requestIdleCallbacks.length === 0) { Timing.setSendIdleEvents(true); diff --git a/Libraries/Core/__mocks__/ErrorUtils.js b/Libraries/Core/__mocks__/ErrorUtils.js index 2719c138c4549d..8505e23dc562fc 100644 --- a/Libraries/Core/__mocks__/ErrorUtils.js +++ b/Libraries/Core/__mocks__/ErrorUtils.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2013-present, Facebook, Inc. + * 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. diff --git a/Libraries/Core/__tests__/MapAndSetPolyfills-test.js b/Libraries/Core/__tests__/MapAndSetPolyfills-test.js new file mode 100644 index 00000000000000..79a73a968789cc --- /dev/null +++ b/Libraries/Core/__tests__/MapAndSetPolyfills-test.js @@ -0,0 +1,104 @@ +/** + * 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'; + +// Save these methods so that we can restore them afterward. +const {freeze, seal, preventExtensions} = Object; + +function setup() { + jest.setMock('../../vendor/core/_shouldPolyfillES6Collection', () => true); + jest.unmock('_wrapObjectFreezeAndFriends'); + require('_wrapObjectFreezeAndFriends'); +} + +function cleanup() { + Object.assign(Object, {freeze, seal, preventExtensions}); +} + +describe('Map polyfill', () => { + setup(); + + const Map = require('Map'); + + it('is not native', () => { + const getCode = Function.prototype.toString.call(Map.prototype.get); + expect(getCode).not.toContain('[native code]'); + expect(getCode).toContain('getIndex'); + }); + + it('should tolerate non-extensible object keys', () => { + const map = new Map(); + const key = Object.create(null); + Object.freeze(key); + map.set(key, key); + expect(map.size).toBe(1); + expect(map.has(key)).toBe(true); + map.delete(key); + expect(map.size).toBe(0); + expect(map.has(key)).toBe(false); + }); + + it('should not get confused by prototypal inheritance', () => { + const map = new Map(); + const proto = Object.create(null); + const base = Object.create(proto); + map.set(proto, proto); + expect(map.size).toBe(1); + expect(map.has(proto)).toBe(true); + expect(map.has(base)).toBe(false); + map.set(base, base); + expect(map.size).toBe(2); + expect(map.get(proto)).toBe(proto); + expect(map.get(base)).toBe(base); + }); + + afterAll(cleanup); +}); + +describe('Set polyfill', () => { + setup(); + + const Set = require('Set'); + + it('is not native', () => { + const addCode = Function.prototype.toString.call(Set.prototype.add); + expect(addCode).not.toContain('[native code]'); + }); + + it('should tolerate non-extensible object elements', () => { + const set = new Set(); + const elem = Object.create(null); + Object.freeze(elem); + set.add(elem); + expect(set.size).toBe(1); + expect(set.has(elem)).toBe(true); + set.add(elem); + expect(set.size).toBe(1); + set.delete(elem); + expect(set.size).toBe(0); + expect(set.has(elem)).toBe(false); + }); + + it('should not get confused by prototypal inheritance', () => { + const set = new Set(); + const proto = Object.create(null); + const base = Object.create(proto); + set.add(proto); + expect(set.size).toBe(1); + expect(set.has(proto)).toBe(true); + expect(set.has(base)).toBe(false); + set.add(base); + expect(set.size).toBe(2); + expect(set.has(proto)).toBe(true); + expect(set.has(base)).toBe(true); + }); + + afterAll(cleanup); +}); diff --git a/Libraries/Core/__tests__/ReactNativeVersionCheck-test.js b/Libraries/Core/__tests__/ReactNativeVersionCheck-test.js index e5c8cefddf15bb..6f4da360362e9f 100644 --- a/Libraries/Core/__tests__/ReactNativeVersionCheck-test.js +++ b/Libraries/Core/__tests__/ReactNativeVersionCheck-test.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2017-present, Facebook, Inc. + * 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. diff --git a/Libraries/Core/checkNativeVersion.js b/Libraries/Core/checkNativeVersion.js new file mode 100644 index 00000000000000..36a8b40ac530ba --- /dev/null +++ b/Libraries/Core/checkNativeVersion.js @@ -0,0 +1,17 @@ +/** + * 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'; + +/** + * Check for compatibility between the JS and native code. + * You can use this module directly, or just require InitializeCore. + */ +const ReactNativeVersionCheck = require('ReactNativeVersionCheck'); +ReactNativeVersionCheck.checkVersions(); diff --git a/Libraries/Core/polyfillES6Collections.js b/Libraries/Core/polyfillES6Collections.js new file mode 100644 index 00000000000000..afbb24ab23ba4e --- /dev/null +++ b/Libraries/Core/polyfillES6Collections.js @@ -0,0 +1,27 @@ +/** + * 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 {polyfillGlobal} = require('PolyfillFunctions'); + +/** + * Polyfill ES6 collections (Map and Set). + * If you don't need these polyfills, don't use InitializeCore; just directly + * require the modules you need from InitializeCore for setup. + */ +const _shouldPolyfillCollection = require('_shouldPolyfillES6Collection'); +if (_shouldPolyfillCollection('Map')) { + require('_wrapObjectFreezeAndFriends'); + polyfillGlobal('Map', () => require('Map')); +} +if (_shouldPolyfillCollection('Set')) { + require('_wrapObjectFreezeAndFriends'); + polyfillGlobal('Set', () => require('Set')); +} diff --git a/Libraries/Core/polyfillPromise.js b/Libraries/Core/polyfillPromise.js new file mode 100644 index 00000000000000..f2068df4e98a02 --- /dev/null +++ b/Libraries/Core/polyfillPromise.js @@ -0,0 +1,21 @@ +/** + * 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 + * @format + */ +'use strict'; + +const {polyfillGlobal} = require('PolyfillFunctions'); + +/** + * Set up Promise. The native Promise implementation throws the following error: + * ERROR: Event loop not supported. + * + * If you don't need these polyfills, don't use InitializeCore; just directly + * require the modules you need from InitializeCore for setup. + */ +polyfillGlobal('Promise', () => require('Promise')); diff --git a/Libraries/Core/setUpAlert.js b/Libraries/Core/setUpAlert.js new file mode 100644 index 00000000000000..0b2bdc9f6a5273 --- /dev/null +++ b/Libraries/Core/setUpAlert.js @@ -0,0 +1,22 @@ +/** + * 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'; + +/** + * Set up alert(). + * You can use this module directly, or just require InitializeCore. + */ +if (!global.alert) { + global.alert = function(text) { + // Require Alert on demand. Requiring it too early can lead to issues + // with things like Platform not being fully initialized. + require('Alert').alert('Alert', '' + text); + }; +} diff --git a/Libraries/Core/setUpBatchedBridge.js b/Libraries/Core/setUpBatchedBridge.js new file mode 100644 index 00000000000000..fd936bf50426a3 --- /dev/null +++ b/Libraries/Core/setUpBatchedBridge.js @@ -0,0 +1,42 @@ +/** + * 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'; + +/** + * Set up the BatchedBridge. This must be done after the other steps in + * InitializeCore to ensure that the JS environment has been initialized. + * You can use this module directly, or just require InitializeCore. + */ +const BatchedBridge = require('BatchedBridge'); +BatchedBridge.registerLazyCallableModule('Systrace', () => require('Systrace')); +BatchedBridge.registerLazyCallableModule('JSTimers', () => require('JSTimers')); +BatchedBridge.registerLazyCallableModule('HeapCapture', () => + require('HeapCapture'), +); +BatchedBridge.registerLazyCallableModule('SamplingProfiler', () => + require('SamplingProfiler'), +); +BatchedBridge.registerLazyCallableModule('RCTLog', () => require('RCTLog')); +BatchedBridge.registerLazyCallableModule('RCTDeviceEventEmitter', () => + require('RCTDeviceEventEmitter'), +); +BatchedBridge.registerLazyCallableModule('RCTNativeAppEventEmitter', () => + require('RCTNativeAppEventEmitter'), +); +BatchedBridge.registerLazyCallableModule('PerformanceLogger', () => + require('PerformanceLogger'), +); +BatchedBridge.registerLazyCallableModule('JSDevSupportModule', () => + require('JSDevSupportModule'), +); + +if (__DEV__ && !global.__RCTProfileIsProfiling) { + BatchedBridge.registerCallableModule('HMRClient', require('HMRClient')); +} diff --git a/Libraries/Core/setUpDeveloperTools.js b/Libraries/Core/setUpDeveloperTools.js new file mode 100644 index 00000000000000..2ee713da0b4dae --- /dev/null +++ b/Libraries/Core/setUpDeveloperTools.js @@ -0,0 +1,28 @@ +/** + * 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'; + +/** + * Sets up developer tools for React Native. + * You can use this module directly, or just require InitializeCore. + */ +if (__DEV__) { + if (!global.__RCTProfileIsProfiling) { + // not when debugging in chrome + // TODO(t12832058) This check is broken + if (!window.document) { + require('setupDevtools'); + } + + // Set up inspector + const JSInspector = require('JSInspector'); + JSInspector.registerAgent(require('NetworkAgent')); + } +} diff --git a/Libraries/Core/setUpErrorHandling.js b/Libraries/Core/setUpErrorHandling.js new file mode 100644 index 00000000000000..7a9c566905d8f9 --- /dev/null +++ b/Libraries/Core/setUpErrorHandling.js @@ -0,0 +1,32 @@ +/** + * 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'; + +/** + * Sets up the console and exception handling (redbox) for React Native. + * You can use this module directly, or just require InitializeCore. + */ +const ExceptionsManager = require('ExceptionsManager'); +ExceptionsManager.installConsoleErrorReporter(); + +// Set up error handler +if (!global.__fbDisableExceptionsManager) { + const handleError = (e, isFatal) => { + try { + ExceptionsManager.handleException(e, isFatal); + } catch (ee) { + console.log('Failed to print error: ', ee.message); + throw e; + } + }; + + const ErrorUtils = require('ErrorUtils'); + ErrorUtils.setGlobalHandler(handleError); +} diff --git a/Libraries/Core/setUpGeolocation.js b/Libraries/Core/setUpGeolocation.js new file mode 100644 index 00000000000000..fd68182bce6bfb --- /dev/null +++ b/Libraries/Core/setUpGeolocation.js @@ -0,0 +1,25 @@ +/** + * 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 {polyfillObjectProperty} = require('PolyfillFunctions'); + +/** + * Set up Geolocation. + * You can use this module directly, or just require InitializeCore. + */ +let navigator = global.navigator; +if (navigator === undefined) { + global.navigator = navigator = {}; +} + +// see https://github.com/facebook/react-native/issues/10881 +polyfillObjectProperty(navigator, 'product', () => 'ReactNative'); +polyfillObjectProperty(navigator, 'geolocation', () => require('Geolocation')); diff --git a/Libraries/Core/setUpGlobals.js b/Libraries/Core/setUpGlobals.js new file mode 100644 index 00000000000000..0310f3c2bcf047 --- /dev/null +++ b/Libraries/Core/setUpGlobals.js @@ -0,0 +1,29 @@ +/** + * 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'; + +/** + * Sets up global variables for React Native. + * You can use this module directly, or just require InitializeCore. + */ +if (global.GLOBAL === undefined) { + global.GLOBAL = global; +} + +if (global.window === undefined) { + global.window = global; +} + +// Set up process +global.process = global.process || {}; +global.process.env = global.process.env || {}; +if (!global.process.env.NODE_ENV) { + global.process.env.NODE_ENV = __DEV__ ? 'development' : 'production'; +} diff --git a/Libraries/Core/setUpRegeneratorRuntime.js b/Libraries/Core/setUpRegeneratorRuntime.js new file mode 100644 index 00000000000000..26fb440c6d270a --- /dev/null +++ b/Libraries/Core/setUpRegeneratorRuntime.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. + * + * @flow strict-local + * @format + */ +'use strict'; + +const {polyfillGlobal} = require('PolyfillFunctions'); + +/** + * Set up regenerator. + * You can use this module directly, or just require InitializeCore. + */ +polyfillGlobal('regeneratorRuntime', () => { + // The require just sets up the global, so make sure when we first + // invoke it the global does not exist + delete global.regeneratorRuntime; + + // regenerator-runtime/runtime exports the regeneratorRuntime object, so we + // can return it safely. + return require('regenerator-runtime/runtime'); // flowlint-line untyped-import:off +}); diff --git a/Libraries/Core/setUpSegmentFetcher.js b/Libraries/Core/setUpSegmentFetcher.js new file mode 100644 index 00000000000000..440caf380f31ac --- /dev/null +++ b/Libraries/Core/setUpSegmentFetcher.js @@ -0,0 +1,42 @@ +/** + * 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'; + +/** + * Set up SegmentFetcher. + * You can use this module directly, or just require InitializeCore. + */ +global.__fetchSegment = function( + segmentId: number, + options: {|+otaBuildNumber: ?string|}, + callback: (?Error) => void, +) { + const {SegmentFetcher} = require('NativeModules'); + if (!SegmentFetcher) { + throw new Error( + 'SegmentFetcher is missing. Please ensure that it is ' + + 'included as a NativeModule.', + ); + } + + SegmentFetcher.fetchSegment( + segmentId, + options, + (errorObject: ?{message: string, code: string}) => { + if (errorObject) { + const error = new Error(errorObject.message); + (error: any).code = errorObject.code; // flowlint-line unclear-type: off + callback(error); + } + + callback(null); + }, + ); +}; diff --git a/Libraries/Core/setUpSystrace.js b/Libraries/Core/setUpSystrace.js new file mode 100644 index 00000000000000..36a6bf40c0ff91 --- /dev/null +++ b/Libraries/Core/setUpSystrace.js @@ -0,0 +1,20 @@ +/** + * 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'; + +/** + * Set up Systrace profiling hooks if necessary. + * You can use this module directly, or just require InitializeCore. + */ +if (global.__RCTProfileIsProfiling) { + const Systrace = require('Systrace'); + Systrace.installReactHook(); + Systrace.setEnabled(true); +} diff --git a/Libraries/Core/setUpTimers.js b/Libraries/Core/setUpTimers.js new file mode 100644 index 00000000000000..1a95afa8751bed --- /dev/null +++ b/Libraries/Core/setUpTimers.js @@ -0,0 +1,30 @@ +/** + * 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 {polyfillGlobal} = require('PolyfillFunctions'); + +/** + * Set up timers. + * You can use this module directly, or just require InitializeCore. + */ +const defineLazyTimer = name => { + polyfillGlobal(name, () => require('JSTimers')[name]); +}; +defineLazyTimer('setTimeout'); +defineLazyTimer('setInterval'); +defineLazyTimer('setImmediate'); +defineLazyTimer('clearTimeout'); +defineLazyTimer('clearInterval'); +defineLazyTimer('clearImmediate'); +defineLazyTimer('requestAnimationFrame'); +defineLazyTimer('cancelAnimationFrame'); +defineLazyTimer('requestIdleCallback'); +defineLazyTimer('cancelIdleCallback'); diff --git a/Libraries/Core/setUpXHR.js b/Libraries/Core/setUpXHR.js new file mode 100644 index 00000000000000..ec7baad8af3dcc --- /dev/null +++ b/Libraries/Core/setUpXHR.js @@ -0,0 +1,32 @@ +/** + * 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 {polyfillGlobal} = require('PolyfillFunctions'); + +/** + * Set up XMLHttpRequest. The native XMLHttpRequest in Chrome dev tools is CORS + * aware and won't let you fetch anything from the internet. + * + * You can use this module directly, or just require InitializeCore. + */ +polyfillGlobal('XMLHttpRequest', () => require('XMLHttpRequest')); +polyfillGlobal('FormData', () => require('FormData')); + +polyfillGlobal('fetch', () => require('fetch').fetch); // flowlint-line untyped-import:off +polyfillGlobal('Headers', () => require('fetch').Headers); // flowlint-line untyped-import:off +polyfillGlobal('Request', () => require('fetch').Request); // flowlint-line untyped-import:off +polyfillGlobal('Response', () => require('fetch').Response); // flowlint-line untyped-import:off +polyfillGlobal('WebSocket', () => require('WebSocket')); +polyfillGlobal('Blob', () => require('Blob')); +polyfillGlobal('File', () => require('File')); +polyfillGlobal('FileReader', () => require('FileReader')); +polyfillGlobal('URL', () => require('URL').URL); // flowlint-line untyped-import:off +polyfillGlobal('URLSearchParams', () => require('URL').URLSearchParams); // flowlint-line untyped-import:off diff --git a/Libraries/DeprecatedPropTypes/DeprecatedColorPropType.js b/Libraries/DeprecatedPropTypes/DeprecatedColorPropType.js new file mode 100644 index 00000000000000..84d3b225a45f33 --- /dev/null +++ b/Libraries/DeprecatedPropTypes/DeprecatedColorPropType.js @@ -0,0 +1,76 @@ +/** + * 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 + */ + +'use strict'; + +const normalizeColor = require('normalizeColor'); + +const colorPropType = function( + isRequired, + props, + propName, + componentName, + location, + propFullName, +) { + const color = props[propName]; + if (color === undefined || color === null) { + if (isRequired) { + return new Error( + 'Required ' + + location + + ' `' + + (propFullName || propName) + + '` was not specified in `' + + componentName + + '`.', + ); + } + return; + } + + if (typeof color === 'number') { + // Developers should not use a number, but we are using the prop type + // both for user provided colors and for transformed ones. This isn't ideal + // and should be fixed but will do for now... + return; + } + + if (normalizeColor(color) === null) { + return new Error( + 'Invalid ' + + location + + ' `' + + (propFullName || propName) + + '` supplied to `' + + componentName + + '`: ' + + color + + '\n' + + `Valid color formats are + - '#f0f' (#rgb) + - '#f0fc' (#rgba) + - '#ff00ff' (#rrggbb) + - '#ff00ff00' (#rrggbbaa) + - 'rgb(255, 255, 255)' + - 'rgba(255, 255, 255, 1.0)' + - 'hsl(360, 100%, 100%)' + - 'hsla(360, 100%, 100%, 1.0)' + - 'transparent' + - 'red' + - 0xff00ff00 (0xrrggbbaa) +`, + ); + } +}; + +const ColorPropType = colorPropType.bind(null, false /* isRequired */); +ColorPropType.isRequired = colorPropType.bind(null, true /* isRequired */); + +module.exports = ColorPropType; diff --git a/Libraries/DeprecatedPropTypes/DeprecatedEdgeInsetsPropType.js b/Libraries/DeprecatedPropTypes/DeprecatedEdgeInsetsPropType.js new file mode 100644 index 00000000000000..67a016fffc8d24 --- /dev/null +++ b/Libraries/DeprecatedPropTypes/DeprecatedEdgeInsetsPropType.js @@ -0,0 +1,22 @@ +/** + * 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 + */ + +'use strict'; + +const PropTypes = require('prop-types'); + +const DeprecatedEdgeInsetsPropType = PropTypes.shape({ + top: PropTypes.number, + left: PropTypes.number, + bottom: PropTypes.number, + right: PropTypes.number, +}); + +module.exports = DeprecatedEdgeInsetsPropType; diff --git a/Libraries/DeprecatedPropTypes/DeprecatedImagePropType.js b/Libraries/DeprecatedPropTypes/DeprecatedImagePropType.js new file mode 100644 index 00000000000000..6b8c16b579d23e --- /dev/null +++ b/Libraries/DeprecatedPropTypes/DeprecatedImagePropType.js @@ -0,0 +1,65 @@ +/** + * 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 + * @format + */ + +'use strict'; + +const DeprecatedEdgeInsetsPropType = require('DeprecatedEdgeInsetsPropType'); +const DeprecatedImageSourcePropType = require('DeprecatedImageSourcePropType'); +const DeprecatedImageStylePropTypes = require('DeprecatedImageStylePropTypes'); +const DeprecatedStyleSheetPropType = require('DeprecatedStyleSheetPropType'); +const PropTypes = require('prop-types'); + +module.exports = { + style: DeprecatedStyleSheetPropType(DeprecatedImageStylePropTypes), + source: DeprecatedImageSourcePropType, + defaultSource: PropTypes.oneOfType([ + PropTypes.shape({ + uri: PropTypes.string, + width: PropTypes.number, + height: PropTypes.number, + scale: PropTypes.number, + }), + PropTypes.number, + ]), + + accessible: PropTypes.bool, + + accessibilityLabel: PropTypes.node, + + blurRadius: PropTypes.number, + + capInsets: DeprecatedEdgeInsetsPropType, + + resizeMethod: PropTypes.oneOf(['auto', 'resize', 'scale']), + + resizeMode: PropTypes.oneOf([ + 'cover', + 'contain', + 'stretch', + 'repeat', + 'center', + ]), + + testID: PropTypes.string, + + onLayout: PropTypes.func, + + onLoadStart: PropTypes.func, + + onProgress: PropTypes.func, + + onError: PropTypes.func, + + onPartialLoad: PropTypes.func, + + onLoad: PropTypes.func, + + onLoadEnd: PropTypes.func, +}; diff --git a/Libraries/DeprecatedPropTypes/DeprecatedImageSourcePropType.js b/Libraries/DeprecatedPropTypes/DeprecatedImageSourcePropType.js new file mode 100644 index 00000000000000..0670023ae73ba2 --- /dev/null +++ b/Libraries/DeprecatedPropTypes/DeprecatedImageSourcePropType.js @@ -0,0 +1,39 @@ +/** + * 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. + * + * @no-flow + * @format + */ +'use strict'; + +const PropTypes = require('prop-types'); + +const ImageURISourcePropType = PropTypes.shape({ + uri: PropTypes.string, + bundle: PropTypes.string, + method: PropTypes.string, + headers: PropTypes.objectOf(PropTypes.string), + body: PropTypes.string, + cache: PropTypes.oneOf([ + 'default', + 'reload', + 'force-cache', + 'only-if-cached', + ]), + width: PropTypes.number, + height: PropTypes.number, + scale: PropTypes.number, +}); + +const ImageSourcePropType = PropTypes.oneOfType([ + ImageURISourcePropType, + // Opaque type returned by require('./image.jpg') + PropTypes.number, + // Multiple sources + PropTypes.arrayOf(ImageURISourcePropType), +]); + +module.exports = ImageSourcePropType; diff --git a/Libraries/DeprecatedPropTypes/DeprecatedImageStylePropTypes.js b/Libraries/DeprecatedPropTypes/DeprecatedImageStylePropTypes.js new file mode 100644 index 00000000000000..3fbed21da30fe0 --- /dev/null +++ b/Libraries/DeprecatedPropTypes/DeprecatedImageStylePropTypes.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. + * + * @flow + * @format + */ +'use strict'; + +const DeprecatedColorPropType = require('DeprecatedColorPropType'); +const DeprecatedLayoutPropTypes = require('DeprecatedLayoutPropTypes'); +const ReactPropTypes = require('prop-types'); +const DeprecatedShadowPropTypesIOS = require('DeprecatedShadowPropTypesIOS'); +const DeprecatedTransformPropTypes = require('DeprecatedTransformPropTypes'); + +const ImageStylePropTypes = { + ...DeprecatedLayoutPropTypes, + ...DeprecatedShadowPropTypesIOS, + ...DeprecatedTransformPropTypes, + resizeMode: ReactPropTypes.oneOf([ + 'center', + 'contain', + 'cover', + 'repeat', + 'stretch', + ]), + backfaceVisibility: ReactPropTypes.oneOf(['visible', 'hidden']), + backgroundColor: DeprecatedColorPropType, + borderColor: DeprecatedColorPropType, + borderWidth: ReactPropTypes.number, + borderRadius: ReactPropTypes.number, + overflow: ReactPropTypes.oneOf(['visible', 'hidden']), + + /** + * Changes the color of all the non-transparent pixels to the tintColor. + */ + tintColor: DeprecatedColorPropType, + opacity: ReactPropTypes.number, + /** + * When the image has rounded corners, specifying an overlayColor will + * cause the remaining space in the corners to be filled with a solid color. + * This is useful in cases which are not supported by the Android + * implementation of rounded corners: + * - Certain resize modes, such as 'contain' + * - Animated GIFs + * + * A typical way to use this prop is with images displayed on a solid + * background and setting the `overlayColor` to the same color + * as the background. + * + * For details of how this works under the hood, see + * http://frescolib.org/docs/rounded-corners-and-circles.html + * + * @platform android + */ + overlayColor: ReactPropTypes.string, + + // Android-Specific styles + borderTopLeftRadius: ReactPropTypes.number, + borderTopRightRadius: ReactPropTypes.number, + borderBottomLeftRadius: ReactPropTypes.number, + borderBottomRightRadius: ReactPropTypes.number, +}; + +module.exports = ImageStylePropTypes; diff --git a/Libraries/DeprecatedPropTypes/DeprecatedLayoutPropTypes.js b/Libraries/DeprecatedPropTypes/DeprecatedLayoutPropTypes.js new file mode 100644 index 00000000000000..0ebccaa8d07083 --- /dev/null +++ b/Libraries/DeprecatedPropTypes/DeprecatedLayoutPropTypes.js @@ -0,0 +1,190 @@ +/** + * 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 + */ + +'use strict'; + +const ReactPropTypes = require('prop-types'); + +const LayoutPropTypes = { + display: ReactPropTypes.oneOf(['none', 'flex']), + width: ReactPropTypes.oneOfType([ + ReactPropTypes.number, + ReactPropTypes.string, + ]), + height: ReactPropTypes.oneOfType([ + ReactPropTypes.number, + ReactPropTypes.string, + ]), + start: ReactPropTypes.oneOfType([ + ReactPropTypes.number, + ReactPropTypes.string, + ]), + end: ReactPropTypes.oneOfType([ReactPropTypes.number, ReactPropTypes.string]), + top: ReactPropTypes.oneOfType([ReactPropTypes.number, ReactPropTypes.string]), + left: ReactPropTypes.oneOfType([ + ReactPropTypes.number, + ReactPropTypes.string, + ]), + right: ReactPropTypes.oneOfType([ + ReactPropTypes.number, + ReactPropTypes.string, + ]), + bottom: ReactPropTypes.oneOfType([ + ReactPropTypes.number, + ReactPropTypes.string, + ]), + minWidth: ReactPropTypes.oneOfType([ + ReactPropTypes.number, + ReactPropTypes.string, + ]), + maxWidth: ReactPropTypes.oneOfType([ + ReactPropTypes.number, + ReactPropTypes.string, + ]), + minHeight: ReactPropTypes.oneOfType([ + ReactPropTypes.number, + ReactPropTypes.string, + ]), + maxHeight: ReactPropTypes.oneOfType([ + ReactPropTypes.number, + ReactPropTypes.string, + ]), + margin: ReactPropTypes.oneOfType([ + ReactPropTypes.number, + ReactPropTypes.string, + ]), + marginVertical: ReactPropTypes.oneOfType([ + ReactPropTypes.number, + ReactPropTypes.string, + ]), + marginHorizontal: ReactPropTypes.oneOfType([ + ReactPropTypes.number, + ReactPropTypes.string, + ]), + marginTop: ReactPropTypes.oneOfType([ + ReactPropTypes.number, + ReactPropTypes.string, + ]), + marginBottom: ReactPropTypes.oneOfType([ + ReactPropTypes.number, + ReactPropTypes.string, + ]), + marginLeft: ReactPropTypes.oneOfType([ + ReactPropTypes.number, + ReactPropTypes.string, + ]), + marginRight: ReactPropTypes.oneOfType([ + ReactPropTypes.number, + ReactPropTypes.string, + ]), + marginStart: ReactPropTypes.oneOfType([ + ReactPropTypes.number, + ReactPropTypes.string, + ]), + marginEnd: ReactPropTypes.oneOfType([ + ReactPropTypes.number, + ReactPropTypes.string, + ]), + padding: ReactPropTypes.oneOfType([ + ReactPropTypes.number, + ReactPropTypes.string, + ]), + paddingVertical: ReactPropTypes.oneOfType([ + ReactPropTypes.number, + ReactPropTypes.string, + ]), + paddingHorizontal: ReactPropTypes.oneOfType([ + ReactPropTypes.number, + ReactPropTypes.string, + ]), + paddingTop: ReactPropTypes.oneOfType([ + ReactPropTypes.number, + ReactPropTypes.string, + ]), + paddingBottom: ReactPropTypes.oneOfType([ + ReactPropTypes.number, + ReactPropTypes.string, + ]), + paddingLeft: ReactPropTypes.oneOfType([ + ReactPropTypes.number, + ReactPropTypes.string, + ]), + paddingRight: ReactPropTypes.oneOfType([ + ReactPropTypes.number, + ReactPropTypes.string, + ]), + paddingStart: ReactPropTypes.oneOfType([ + ReactPropTypes.number, + ReactPropTypes.string, + ]), + paddingEnd: ReactPropTypes.oneOfType([ + ReactPropTypes.number, + ReactPropTypes.string, + ]), + borderWidth: ReactPropTypes.number, + borderTopWidth: ReactPropTypes.number, + borderStartWidth: ReactPropTypes.number, + borderEndWidth: ReactPropTypes.number, + borderRightWidth: ReactPropTypes.number, + borderBottomWidth: ReactPropTypes.number, + borderLeftWidth: ReactPropTypes.number, + position: ReactPropTypes.oneOf(['absolute', 'relative']), + flexDirection: ReactPropTypes.oneOf([ + 'row', + 'row-reverse', + 'column', + 'column-reverse', + ]), + flexWrap: ReactPropTypes.oneOf(['wrap', 'nowrap', 'wrap-reverse']), + justifyContent: ReactPropTypes.oneOf([ + 'flex-start', + 'flex-end', + 'center', + 'space-between', + 'space-around', + 'space-evenly', + ]), + alignItems: ReactPropTypes.oneOf([ + 'flex-start', + 'flex-end', + 'center', + 'stretch', + 'baseline', + ]), + alignSelf: ReactPropTypes.oneOf([ + 'auto', + 'flex-start', + 'flex-end', + 'center', + 'stretch', + 'baseline', + ]), + alignContent: ReactPropTypes.oneOf([ + 'flex-start', + 'flex-end', + 'center', + 'stretch', + 'space-between', + 'space-around', + ]), + overflow: ReactPropTypes.oneOf(['visible', 'hidden', 'scroll']), + flex: ReactPropTypes.number, + flexGrow: ReactPropTypes.number, + flexShrink: ReactPropTypes.number, + flexBasis: ReactPropTypes.oneOfType([ + ReactPropTypes.number, + ReactPropTypes.string, + ]), + aspectRatio: ReactPropTypes.number, + zIndex: ReactPropTypes.number, + direction: ReactPropTypes.oneOf(['inherit', 'ltr', 'rtl']), +}; + +module.exports = LayoutPropTypes; diff --git a/Libraries/DeprecatedPropTypes/DeprecatedPointPropType.js b/Libraries/DeprecatedPropTypes/DeprecatedPointPropType.js new file mode 100644 index 00000000000000..9ae8c8f9872d5c --- /dev/null +++ b/Libraries/DeprecatedPropTypes/DeprecatedPointPropType.js @@ -0,0 +1,20 @@ +/** + * 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 + */ + +'use strict'; + +const PropTypes = require('prop-types'); + +const PointPropType = PropTypes.shape({ + x: PropTypes.number, + y: PropTypes.number, +}); + +module.exports = PointPropType; diff --git a/Libraries/DeprecatedPropTypes/DeprecatedShadowPropTypesIOS.js b/Libraries/DeprecatedPropTypes/DeprecatedShadowPropTypesIOS.js new file mode 100644 index 00000000000000..889b5624938736 --- /dev/null +++ b/Libraries/DeprecatedPropTypes/DeprecatedShadowPropTypesIOS.js @@ -0,0 +1,25 @@ +/** + * 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 + * @format + */ +'use strict'; + +const DeprecatedColorPropType = require('DeprecatedColorPropType'); +const ReactPropTypes = require('prop-types'); + +const DeprecatedShadowPropTypesIOS = { + shadowColor: DeprecatedColorPropType, + shadowOffset: ReactPropTypes.shape({ + width: ReactPropTypes.number, + height: ReactPropTypes.number, + }), + shadowOpacity: ReactPropTypes.number, + shadowRadius: ReactPropTypes.number, +}; + +module.exports = DeprecatedShadowPropTypesIOS; diff --git a/Libraries/DeprecatedPropTypes/DeprecatedStyleSheetPropType.js b/Libraries/DeprecatedPropTypes/DeprecatedStyleSheetPropType.js new file mode 100644 index 00000000000000..9cad5470ad7374 --- /dev/null +++ b/Libraries/DeprecatedPropTypes/DeprecatedStyleSheetPropType.js @@ -0,0 +1,31 @@ +/** + * 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'; + +const deprecatedCreateStrictShapeTypeChecker = require('deprecatedCreateStrictShapeTypeChecker'); +const flattenStyle = require('flattenStyle'); + +function DeprecatedStyleSheetPropType(shape: { + [key: string]: ReactPropsCheckType, +}): ReactPropsCheckType { + const shapePropType = deprecatedCreateStrictShapeTypeChecker(shape); + return function(props, propName, componentName, location?, ...rest) { + let newProps = props; + if (props[propName]) { + // Just make a dummy prop object with only the flattened style + newProps = {}; + newProps[propName] = flattenStyle(props[propName]); + } + return shapePropType(newProps, propName, componentName, location, ...rest); + }; +} + +module.exports = DeprecatedStyleSheetPropType; diff --git a/Libraries/DeprecatedPropTypes/DeprecatedTVViewPropTypes.js b/Libraries/DeprecatedPropTypes/DeprecatedTVViewPropTypes.js new file mode 100644 index 00000000000000..90af60cc27da35 --- /dev/null +++ b/Libraries/DeprecatedPropTypes/DeprecatedTVViewPropTypes.js @@ -0,0 +1,25 @@ +/** + * 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 PropTypes = require('prop-types'); + +const DeprecatedTVViewPropTypes = { + isTVSelectable: PropTypes.bool, + hasTVPreferredFocus: PropTypes.bool, + tvParallaxProperties: PropTypes.object, + tvParallaxShiftDistanceX: PropTypes.number, + tvParallaxShiftDistanceY: PropTypes.number, + tvParallaxTiltAngle: PropTypes.number, + tvParallaxMagnification: PropTypes.number, +}; + +module.exports = DeprecatedTVViewPropTypes; diff --git a/Libraries/DeprecatedPropTypes/DeprecatedTextPropTypes.js b/Libraries/DeprecatedPropTypes/DeprecatedTextPropTypes.js new file mode 100644 index 00000000000000..1723bb56cb6cdd --- /dev/null +++ b/Libraries/DeprecatedPropTypes/DeprecatedTextPropTypes.js @@ -0,0 +1,41 @@ +/** + * 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 + * @format + */ + +'use strict'; + +const DeprecatedColorPropType = require('DeprecatedColorPropType'); +const DeprecatedEdgeInsetsPropType = require('DeprecatedEdgeInsetsPropType'); +const DeprecatedStyleSheetPropType = require('DeprecatedStyleSheetPropType'); +const PropTypes = require('prop-types'); +const TextStylePropTypes = require('TextStylePropTypes'); + +const stylePropType = DeprecatedStyleSheetPropType(TextStylePropTypes); + +module.exports = { + ellipsizeMode: PropTypes.oneOf(['head', 'middle', 'tail', 'clip']), + numberOfLines: PropTypes.number, + textBreakStrategy: PropTypes.oneOf(['simple', 'highQuality', 'balanced']), + onLayout: PropTypes.func, + onPress: PropTypes.func, + onLongPress: PropTypes.func, + pressRetentionOffset: DeprecatedEdgeInsetsPropType, + selectable: PropTypes.bool, + selectionColor: DeprecatedColorPropType, + suppressHighlighting: PropTypes.bool, + style: stylePropType, + testID: PropTypes.string, + nativeID: PropTypes.string, + allowFontScaling: PropTypes.bool, + maxFontSizeMultiplier: PropTypes.number, + accessible: PropTypes.bool, + adjustsFontSizeToFit: PropTypes.bool, + minimumFontScale: PropTypes.number, + disabled: PropTypes.bool, +}; diff --git a/Libraries/DeprecatedPropTypes/DeprecatedTransformPropTypes.js b/Libraries/DeprecatedPropTypes/DeprecatedTransformPropTypes.js new file mode 100644 index 00000000000000..a463dcc1cd15db --- /dev/null +++ b/Libraries/DeprecatedPropTypes/DeprecatedTransformPropTypes.js @@ -0,0 +1,84 @@ +/** + * 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 ReactPropTypes = require('prop-types'); + +const deprecatedPropType = require('deprecatedPropType'); + +const TransformMatrixPropType = function( + props: Object, + propName: string, + componentName: string, +): ?Error { + if (props[propName]) { + return new Error( + 'The transformMatrix style property is deprecated. ' + + 'Use `transform: [{ matrix: ... }]` instead.', + ); + } +}; + +const DecomposedMatrixPropType = function( + props: Object, + propName: string, + componentName: string, +): ?Error { + if (props[propName]) { + return new Error( + 'The decomposedMatrix style property is deprecated. ' + + 'Use `transform: [...]` instead.', + ); + } +}; + +const DeprecatedTransformPropTypes = { + transform: ReactPropTypes.arrayOf( + ReactPropTypes.oneOfType([ + ReactPropTypes.shape({perspective: ReactPropTypes.number}), + ReactPropTypes.shape({rotate: ReactPropTypes.string}), + ReactPropTypes.shape({rotateX: ReactPropTypes.string}), + ReactPropTypes.shape({rotateY: ReactPropTypes.string}), + ReactPropTypes.shape({rotateZ: ReactPropTypes.string}), + ReactPropTypes.shape({scale: ReactPropTypes.number}), + ReactPropTypes.shape({scaleX: ReactPropTypes.number}), + ReactPropTypes.shape({scaleY: ReactPropTypes.number}), + ReactPropTypes.shape({translateX: ReactPropTypes.number}), + ReactPropTypes.shape({translateY: ReactPropTypes.number}), + ReactPropTypes.shape({skewX: ReactPropTypes.string}), + ReactPropTypes.shape({skewY: ReactPropTypes.string}), + ]), + ), + transformMatrix: TransformMatrixPropType, + decomposedMatrix: DecomposedMatrixPropType, + scaleX: deprecatedPropType( + ReactPropTypes.number, + 'Use the transform prop instead.', + ), + scaleY: deprecatedPropType( + ReactPropTypes.number, + 'Use the transform prop instead.', + ), + rotation: deprecatedPropType( + ReactPropTypes.number, + 'Use the transform prop instead.', + ), + translateX: deprecatedPropType( + ReactPropTypes.number, + 'Use the transform prop instead.', + ), + translateY: deprecatedPropType( + ReactPropTypes.number, + 'Use the transform prop instead.', + ), +}; + +module.exports = DeprecatedTransformPropTypes; diff --git a/Libraries/DeprecatedPropTypes/DeprecatedViewAccessibility.js b/Libraries/DeprecatedPropTypes/DeprecatedViewAccessibility.js new file mode 100644 index 00000000000000..890dd49f0f8672 --- /dev/null +++ b/Libraries/DeprecatedPropTypes/DeprecatedViewAccessibility.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 + * @flow strict + */ + +'use strict'; + +module.exports = { + DeprecatedAccessibilityTraits: [ + 'none', + 'button', + 'link', + 'header', + 'search', + 'image', + 'selected', + 'plays', + 'key', + 'text', + 'summary', + 'disabled', + 'frequentUpdates', + 'startsMedia', + 'adjustable', + 'allowsDirectInteraction', + 'pageTurn', + ], + DeprecatedAccessibilityComponentTypes: [ + 'none', + 'button', + 'radiobutton_checked', + 'radiobutton_unchecked', + ], + // This must be kept in sync with the AccessibilityRolesMask in RCTViewManager.m + DeprecatedAccessibilityRoles: [ + 'none', + 'button', + 'link', + 'search', + 'image', + 'keyboardkey', + 'text', + 'adjustable', + 'imagebutton', + 'header', + 'summary', + ], + // This must be kept in sync with the AccessibilityStatesMask in RCTViewManager.m + DeprecatedAccessibilityStates: ['selected', 'disabled'], +}; diff --git a/Libraries/DeprecatedPropTypes/DeprecatedViewPropTypes.js b/Libraries/DeprecatedPropTypes/DeprecatedViewPropTypes.js new file mode 100644 index 00000000000000..0d5b0746c4a3d8 --- /dev/null +++ b/Libraries/DeprecatedPropTypes/DeprecatedViewPropTypes.js @@ -0,0 +1,409 @@ +/** + * 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 DeprecatedEdgeInsetsPropType = require('DeprecatedEdgeInsetsPropType'); +const PlatformViewPropTypes = require('PlatformViewPropTypes'); +const PropTypes = require('prop-types'); +const DeprecatedStyleSheetPropType = require('DeprecatedStyleSheetPropType'); +const DeprecatedViewStylePropTypes = require('DeprecatedViewStylePropTypes'); + +const { + DeprecatedAccessibilityComponentTypes, + DeprecatedAccessibilityTraits, + DeprecatedAccessibilityRoles, + DeprecatedAccessibilityStates, +} = require('DeprecatedViewAccessibility'); + +const stylePropType = DeprecatedStyleSheetPropType( + DeprecatedViewStylePropTypes, +); + +module.exports = { + /** + * When `true`, indicates that the view is an accessibility element. + * By default, all the touchable elements are accessible. + * + * See http://facebook.github.io/react-native/docs/view.html#accessible + */ + accessible: PropTypes.bool, + + /** + * Overrides the text that's read by the screen reader when the user interacts + * with the element. By default, the label is constructed by traversing all + * the children and accumulating all the `Text` nodes separated by space. + * + * See http://facebook.github.io/react-native/docs/view.html#accessibilitylabel + */ + accessibilityLabel: PropTypes.node, + + /** + * An accessibility hint helps users understand what will happen when they perform + * an action on the accessibility element when that result is not obvious from the + * accessibility label. + * + * + * See http://facebook.github.io/react-native/docs/view.html#accessibilityHint + */ + accessibilityHint: PropTypes.string, + + /** + * Provides an array of custom actions available for accessibility. + * + * @platform ios + */ + accessibilityActions: PropTypes.arrayOf(PropTypes.string), + + /** + * Prevents view from being inverted if set to true and color inversion is turned on. + * + * @platform ios + */ + accessibilityIgnoresInvertColors: PropTypes.bool, + + /** + * Indicates to accessibility services to treat UI component like a + * native one. Works for Android only. + * + * @platform android + * + * See http://facebook.github.io/react-native/docs/view.html#accessibilitycomponenttype + */ + accessibilityComponentType: PropTypes.oneOf( + DeprecatedAccessibilityComponentTypes, + ), + + /** + * Indicates to accessibility services to treat UI component like a specific role. + */ + accessibilityRole: PropTypes.oneOf(DeprecatedAccessibilityRoles), + + /** + * Indicates to accessibility services that UI Component is in a specific State. + */ + accessibilityStates: PropTypes.arrayOf( + PropTypes.oneOf(DeprecatedAccessibilityStates), + ), + /** + * Indicates to accessibility services whether the user should be notified + * when this view changes. Works for Android API >= 19 only. + * + * @platform android + * + * See http://facebook.github.io/react-native/docs/view.html#accessibilityliveregion + */ + accessibilityLiveRegion: PropTypes.oneOf(['none', 'polite', 'assertive']), + + /** + * Controls how view is important for accessibility which is if it + * fires accessibility events and if it is reported to accessibility services + * that query the screen. Works for Android only. + * + * @platform android + * + * See http://facebook.github.io/react-native/docs/view.html#importantforaccessibility + */ + importantForAccessibility: PropTypes.oneOf([ + 'auto', + 'yes', + 'no', + 'no-hide-descendants', + ]), + + /** + * Provides additional traits to screen reader. By default no traits are + * provided unless specified otherwise in element. + * + * You can provide one trait or an array of many traits. + * + * @platform ios + * + * See http://facebook.github.io/react-native/docs/view.html#accessibilitytraits + */ + accessibilityTraits: PropTypes.oneOfType([ + PropTypes.oneOf(DeprecatedAccessibilityTraits), + PropTypes.arrayOf(PropTypes.oneOf(DeprecatedAccessibilityTraits)), + ]), + + /** + * A value indicating whether VoiceOver should ignore the elements + * within views that are siblings of the receiver. + * Default is `false`. + * + * @platform ios + * + * See http://facebook.github.io/react-native/docs/view.html#accessibilityviewismodal + */ + accessibilityViewIsModal: PropTypes.bool, + + /** + * A value indicating whether the accessibility elements contained within + * this accessibility element are hidden. + * + * @platform ios + * + * See http://facebook.github.io/react-native/docs/view.html#accessibilityElementsHidden + */ + accessibilityElementsHidden: PropTypes.bool, + + /** + * When `accessible` is true, the system will try to invoke this function + * when the user performs an accessibility custom action. + * + * @platform ios + */ + onAccessibilityAction: PropTypes.func, + + /** + * When `accessible` is true, the system will try to invoke this function + * when the user performs accessibility tap gesture. + * + * See http://facebook.github.io/react-native/docs/view.html#onaccessibilitytap + */ + onAccessibilityTap: PropTypes.func, + + /** + * When `accessible` is `true`, the system will invoke this function when the + * user performs the magic tap gesture. + * + * See http://facebook.github.io/react-native/docs/view.html#onmagictap + */ + onMagicTap: PropTypes.func, + + /** + * Used to locate this view in end-to-end tests. + * + * > This disables the 'layout-only view removal' optimization for this view! + * + * See http://facebook.github.io/react-native/docs/view.html#testid + */ + testID: PropTypes.string, + + /** + * Used to locate this view from native classes. + * + * > This disables the 'layout-only view removal' optimization for this view! + * + * See http://facebook.github.io/react-native/docs/view.html#nativeid + */ + nativeID: PropTypes.string, + + /** + * For most touch interactions, you'll simply want to wrap your component in + * `TouchableHighlight` or `TouchableOpacity`. Check out `Touchable.js`, + * `ScrollResponder.js` and `ResponderEventPlugin.js` for more discussion. + */ + + /** + * The View is now responding for touch events. This is the time to highlight + * and show the user what is happening. + * + * `View.props.onResponderGrant: (event) => {}`, where `event` is a synthetic + * touch event as described above. + * + * See http://facebook.github.io/react-native/docs/view.html#onrespondergrant + */ + onResponderGrant: PropTypes.func, + + /** + * The user is moving their finger. + * + * `View.props.onResponderMove: (event) => {}`, where `event` is a synthetic + * touch event as described above. + * + * See http://facebook.github.io/react-native/docs/view.html#onrespondermove + */ + onResponderMove: PropTypes.func, + + /** + * Another responder is already active and will not release it to that `View` + * asking to be the responder. + * + * `View.props.onResponderReject: (event) => {}`, where `event` is a + * synthetic touch event as described above. + * + * See http://facebook.github.io/react-native/docs/view.html#onresponderreject + */ + onResponderReject: PropTypes.func, + + /** + * Fired at the end of the touch. + * + * `View.props.onResponderRelease: (event) => {}`, where `event` is a + * synthetic touch event as described above. + * + * See http://facebook.github.io/react-native/docs/view.html#onresponderrelease + */ + onResponderRelease: PropTypes.func, + + /** + * The responder has been taken from the `View`. Might be taken by other + * views after a call to `onResponderTerminationRequest`, or might be taken + * by the OS without asking (e.g., happens with control center/ notification + * center on iOS) + * + * `View.props.onResponderTerminate: (event) => {}`, where `event` is a + * synthetic touch event as described above. + * + * See http://facebook.github.io/react-native/docs/view.html#onresponderterminate + */ + onResponderTerminate: PropTypes.func, + + /** + * Some other `View` wants to become responder and is asking this `View` to + * release its responder. Returning `true` allows its release. + * + * `View.props.onResponderTerminationRequest: (event) => {}`, where `event` + * is a synthetic touch event as described above. + * + * See http://facebook.github.io/react-native/docs/view.html#onresponderterminationrequest + */ + onResponderTerminationRequest: PropTypes.func, + + /** + * Does this view want to become responder on the start of a touch? + * + * `View.props.onStartShouldSetResponder: (event) => [true | false]`, where + * `event` is a synthetic touch event as described above. + * + * See http://facebook.github.io/react-native/docs/view.html#onstartshouldsetresponder + */ + onStartShouldSetResponder: PropTypes.func, + + /** + * If a parent `View` wants to prevent a child `View` from becoming responder + * on a touch start, it should have this handler which returns `true`. + * + * `View.props.onStartShouldSetResponderCapture: (event) => [true | false]`, + * where `event` is a synthetic touch event as described above. + * + * See http://facebook.github.io/react-native/docs/view.html#onstartshouldsetrespondercapture + */ + onStartShouldSetResponderCapture: PropTypes.func, + + /** + * Does this view want to "claim" touch responsiveness? This is called for + * every touch move on the `View` when it is not the responder. + * + * `View.props.onMoveShouldSetResponder: (event) => [true | false]`, where + * `event` is a synthetic touch event as described above. + * + * See http://facebook.github.io/react-native/docs/view.html#onmoveshouldsetresponder + */ + onMoveShouldSetResponder: PropTypes.func, + + /** + * If a parent `View` wants to prevent a child `View` from becoming responder + * on a move, it should have this handler which returns `true`. + * + * `View.props.onMoveShouldSetResponderCapture: (event) => [true | false]`, + * where `event` is a synthetic touch event as described above. + * + * See http://facebook.github.io/react-native/docs/view.html#onMoveShouldsetrespondercapture + */ + onMoveShouldSetResponderCapture: PropTypes.func, + + /** + * This defines how far a touch event can start away from the view. + * Typical interface guidelines recommend touch targets that are at least + * 30 - 40 points/density-independent pixels. + * + * > The touch area never extends past the parent view bounds and the Z-index + * > of sibling views always takes precedence if a touch hits two overlapping + * > views. + * + * See http://facebook.github.io/react-native/docs/view.html#hitslop + */ + hitSlop: DeprecatedEdgeInsetsPropType, + + /** + * Invoked on mount and layout changes with: + * + * `{nativeEvent: { layout: {x, y, width, height}}}` + * + * This event is fired immediately once the layout has been calculated, but + * the new layout may not yet be reflected on the screen at the time the + * event is received, especially if a layout animation is in progress. + * + * See http://facebook.github.io/react-native/docs/view.html#onlayout + */ + onLayout: PropTypes.func, + + /** + * Controls whether the `View` can be the target of touch events. + * + * See http://facebook.github.io/react-native/docs/view.html#pointerevents + */ + pointerEvents: PropTypes.oneOf(['box-none', 'none', 'box-only', 'auto']), + + /** + * See http://facebook.github.io/react-native/docs/style.html + */ + style: stylePropType, + + /** + * This is a special performance property exposed by `RCTView` and is useful + * for scrolling content when there are many subviews, most of which are + * offscreen. For this property to be effective, it must be applied to a + * view that contains many subviews that extend outside its bound. The + * subviews must also have `overflow: hidden`, as should the containing view + * (or one of its superviews). + * + * See http://facebook.github.io/react-native/docs/view.html#removeclippedsubviews + */ + removeClippedSubviews: PropTypes.bool, + + /** + * Whether this `View` should render itself (and all of its children) into a + * single hardware texture on the GPU. + * + * @platform android + * + * See http://facebook.github.io/react-native/docs/view.html#rendertohardwaretextureandroid + */ + renderToHardwareTextureAndroid: PropTypes.bool, + + /** + * Whether this `View` should be rendered as a bitmap before compositing. + * + * @platform ios + * + * See http://facebook.github.io/react-native/docs/view.html#shouldrasterizeios + */ + shouldRasterizeIOS: PropTypes.bool, + + /** + * Views that are only used to layout their children or otherwise don't draw + * anything may be automatically removed from the native hierarchy as an + * optimization. Set this property to `false` to disable this optimization and + * ensure that this `View` exists in the native view hierarchy. + * + * @platform android + * + * See http://facebook.github.io/react-native/docs/view.html#collapsable + */ + collapsable: PropTypes.bool, + + /** + * Whether this `View` needs to rendered offscreen and composited with an + * alpha in order to preserve 100% correct colors and blending behavior. + * + * @platform android + * + * See http://facebook.github.io/react-native/docs/view.html#needsoffscreenalphacompositing + */ + needsOffscreenAlphaCompositing: PropTypes.bool, + + /** + * Any additional platform-specific view prop types, or prop type overrides. + */ + ...PlatformViewPropTypes, +}; diff --git a/Libraries/DeprecatedPropTypes/DeprecatedViewStylePropTypes.js b/Libraries/DeprecatedPropTypes/DeprecatedViewStylePropTypes.js new file mode 100644 index 00000000000000..546da9cee33e8d --- /dev/null +++ b/Libraries/DeprecatedPropTypes/DeprecatedViewStylePropTypes.js @@ -0,0 +1,61 @@ +/** + * 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 DeprecatedColorPropType = require('DeprecatedColorPropType'); +const DeprecatedLayoutPropTypes = require('DeprecatedLayoutPropTypes'); +const ReactPropTypes = require('prop-types'); +const DeprecatedShadowPropTypesIOS = require('DeprecatedShadowPropTypesIOS'); +const DeprecatedTransformPropTypes = require('DeprecatedTransformPropTypes'); + +/** + * Warning: Some of these properties may not be supported in all releases. + */ +const DeprecatedViewStylePropTypes = { + ...DeprecatedLayoutPropTypes, + ...DeprecatedShadowPropTypesIOS, + ...DeprecatedTransformPropTypes, + backfaceVisibility: ReactPropTypes.oneOf(['visible', 'hidden']), + backgroundColor: DeprecatedColorPropType, + borderColor: DeprecatedColorPropType, + borderTopColor: DeprecatedColorPropType, + borderRightColor: DeprecatedColorPropType, + borderBottomColor: DeprecatedColorPropType, + borderLeftColor: DeprecatedColorPropType, + borderStartColor: DeprecatedColorPropType, + borderEndColor: DeprecatedColorPropType, + borderRadius: ReactPropTypes.number, + borderTopLeftRadius: ReactPropTypes.number, + borderTopRightRadius: ReactPropTypes.number, + borderTopStartRadius: ReactPropTypes.number, + borderTopEndRadius: ReactPropTypes.number, + borderBottomLeftRadius: ReactPropTypes.number, + borderBottomRightRadius: ReactPropTypes.number, + borderBottomStartRadius: ReactPropTypes.number, + borderBottomEndRadius: ReactPropTypes.number, + borderStyle: ReactPropTypes.oneOf(['solid', 'dotted', 'dashed']), + borderWidth: ReactPropTypes.number, + borderTopWidth: ReactPropTypes.number, + borderRightWidth: ReactPropTypes.number, + borderBottomWidth: ReactPropTypes.number, + borderLeftWidth: ReactPropTypes.number, + opacity: ReactPropTypes.number, + /** + * (Android-only) Sets the elevation of a view, using Android's underlying + * [elevation API](https://developer.android.com/training/material/shadows-clipping.html#Elevation). + * This adds a drop shadow to the item and affects z-order for overlapping views. + * Only supported on Android 5.0+, has no effect on earlier versions. + * @platform android + */ + elevation: ReactPropTypes.number, +}; + +module.exports = DeprecatedViewStylePropTypes; diff --git a/Libraries/DeprecatedPropTypes/deprecatedCreateStrictShapeTypeChecker.js b/Libraries/DeprecatedPropTypes/deprecatedCreateStrictShapeTypeChecker.js new file mode 100644 index 00000000000000..c28b2288e2d434 --- /dev/null +++ b/Libraries/DeprecatedPropTypes/deprecatedCreateStrictShapeTypeChecker.js @@ -0,0 +1,86 @@ +/** + * 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 invariant = require('invariant'); +const merge = require('merge'); + +function deprecatedCreateStrictShapeTypeChecker(shapeTypes: { + [key: string]: ReactPropsCheckType, +}): ReactPropsChainableTypeChecker { + function checkType( + isRequired, + props, + propName, + componentName, + location?, + ...rest + ) { + if (!props[propName]) { + if (isRequired) { + invariant( + false, + `Required object \`${propName}\` was not specified in ` + + `\`${componentName}\`.`, + ); + } + return; + } + const propValue = props[propName]; + const propType = typeof propValue; + const locationName = location || '(unknown)'; + if (propType !== 'object') { + invariant( + false, + `Invalid ${locationName} \`${propName}\` of type \`${propType}\` ` + + `supplied to \`${componentName}\`, expected \`object\`.`, + ); + } + // We need to check all keys in case some are required but missing from + // props. + const allKeys = merge(props[propName], shapeTypes); + for (const key in allKeys) { + const checker = shapeTypes[key]; + if (!checker) { + invariant( + false, + `Invalid props.${propName} key \`${key}\` supplied to \`${componentName}\`.` + + '\nBad object: ' + + JSON.stringify(props[propName], null, ' ') + + '\nValid keys: ' + + JSON.stringify(Object.keys(shapeTypes), null, ' '), + ); + } + const error = checker(propValue, key, componentName, location, ...rest); + if (error) { + invariant( + false, + error.message + + '\nBad object: ' + + JSON.stringify(props[propName], null, ' '), + ); + } + } + } + function chainedCheckType( + props: {[key: string]: any}, + propName: string, + componentName: string, + location?: string, + ...rest + ): ?Error { + return checkType(false, props, propName, componentName, location, ...rest); + } + chainedCheckType.isRequired = checkType.bind(null, true); + return chainedCheckType; +} + +module.exports = deprecatedCreateStrictShapeTypeChecker; diff --git a/Libraries/EventEmitter/MissingNativeEventEmitterShim.js b/Libraries/EventEmitter/MissingNativeEventEmitterShim.js index 562964b85f4cf4..b7568027876af4 100644 --- a/Libraries/EventEmitter/MissingNativeEventEmitterShim.js +++ b/Libraries/EventEmitter/MissingNativeEventEmitterShim.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -13,7 +13,7 @@ const EmitterSubscription = require('EmitterSubscription'); const EventEmitter = require('EventEmitter'); -const invariant = require('fbjs/lib/invariant'); +const invariant = require('invariant'); class MissingNativeEventEmitterShim extends EventEmitter { isAvailable: boolean = false; diff --git a/Libraries/EventEmitter/NativeEventEmitter.js b/Libraries/EventEmitter/NativeEventEmitter.js index 2220670fc76909..62b85c69a8e974 100644 --- a/Libraries/EventEmitter/NativeEventEmitter.js +++ b/Libraries/EventEmitter/NativeEventEmitter.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -14,7 +14,7 @@ const EventEmitter = require('EventEmitter'); const Platform = require('Platform'); const RCTDeviceEventEmitter = require('RCTDeviceEventEmitter'); -const invariant = require('fbjs/lib/invariant'); +const invariant = require('invariant'); import type EmitterSubscription from 'EmitterSubscription'; diff --git a/Libraries/EventEmitter/RCTDeviceEventEmitter.js b/Libraries/EventEmitter/RCTDeviceEventEmitter.js index b0977f219abc25..342aa733bf08ff 100644 --- a/Libraries/EventEmitter/RCTDeviceEventEmitter.js +++ b/Libraries/EventEmitter/RCTDeviceEventEmitter.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/EventEmitter/RCTEventEmitter.js b/Libraries/EventEmitter/RCTEventEmitter.js index 1a51905ba20d1e..f84bca058ed366 100644 --- a/Libraries/EventEmitter/RCTEventEmitter.js +++ b/Libraries/EventEmitter/RCTEventEmitter.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/EventEmitter/RCTNativeAppEventEmitter.js b/Libraries/EventEmitter/RCTNativeAppEventEmitter.js index a32cfcef7cf9c7..2d302c014feebb 100644 --- a/Libraries/EventEmitter/RCTNativeAppEventEmitter.js +++ b/Libraries/EventEmitter/RCTNativeAppEventEmitter.js @@ -1,11 +1,11 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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 + * @flow strict-local */ 'use strict'; diff --git a/Libraries/EventEmitter/__mocks__/NativeEventEmitter.js b/Libraries/EventEmitter/__mocks__/NativeEventEmitter.js index e822b72c73c192..e34e60e2e9f201 100644 --- a/Libraries/EventEmitter/__mocks__/NativeEventEmitter.js +++ b/Libraries/EventEmitter/__mocks__/NativeEventEmitter.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/Experimental/Incremental.js b/Libraries/Experimental/Incremental.js index cfd0c69fd03cfe..76d8ff902680ab 100644 --- a/Libraries/Experimental/Incremental.js +++ b/Libraries/Experimental/Incremental.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -90,14 +90,12 @@ export type Props = { * Tags instances and associated tasks for easier debugging. */ name: string, - children?: any, -}; -type DefaultProps = { - name: string, + children: React.Node, }; type State = { doIncrementalRender: boolean, }; + class Incremental extends React.Component { props: Props; state: State; diff --git a/Libraries/Experimental/IncrementalExample.js b/Libraries/Experimental/IncrementalExample.js index 3cc441fff38591..e63f59a996f974 100644 --- a/Libraries/Experimental/IncrementalExample.js +++ b/Libraries/Experimental/IncrementalExample.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/Experimental/IncrementalGroup.js b/Libraries/Experimental/IncrementalGroup.js index af1a7ad63ddb79..03421e92c58f4c 100644 --- a/Libraries/Experimental/IncrementalGroup.js +++ b/Libraries/Experimental/IncrementalGroup.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/Experimental/IncrementalPresenter.js b/Libraries/Experimental/IncrementalPresenter.js index 9927179d173966..784878f93468d4 100644 --- a/Libraries/Experimental/IncrementalPresenter.js +++ b/Libraries/Experimental/IncrementalPresenter.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -11,13 +11,13 @@ 'use strict'; const IncrementalGroup = require('IncrementalGroup'); -const React = require('React'); const PropTypes = require('prop-types'); +const React = require('React'); const View = require('View'); -const ViewPropTypes = require('ViewPropTypes'); - import type {Context} from 'Incremental'; +import type {ViewStyleProp} from 'StyleSheet'; +import type {LayoutEvent} from 'CoreEventTypes'; /** * WARNING: EXPERIMENTAL. Breaking changes will probably happen a lot and will @@ -31,25 +31,19 @@ import type {Context} from 'Incremental'; * * See Incremental.js for more info. */ -type Props = { +type Props = $ReadOnly<{| name: string, disabled?: boolean, - onDone?: () => void, - onLayout?: (event: Object) => void, - style?: mixed, - children?: any, -}; + onDone?: () => mixed, + onLayout?: (event: LayoutEvent) => mixed, + style?: ViewStyleProp, + children?: React.Node, +|}>; + class IncrementalPresenter extends React.Component { context: Context; _isDone: boolean; - static propTypes = { - name: PropTypes.string, - disabled: PropTypes.bool, - onDone: PropTypes.func, - onLayout: PropTypes.func, - style: ViewPropTypes.style, - }; static contextTypes = { incrementalGroup: PropTypes.object, incrementalGroupEnabled: PropTypes.bool, @@ -74,14 +68,15 @@ class IncrementalPresenter extends React.Component { this.props.onDone && this.props.onDone(); } render() { + let style: ViewStyleProp; if ( this.props.disabled !== true && this.context.incrementalGroupEnabled !== false && !this._isDone ) { - var style = [this.props.style, {opacity: 0, position: 'absolute'}]; + style = [this.props.style, {opacity: 0, position: 'absolute'}]; } else { - var style = this.props.style; + style = this.props.style; } return ( number), - // Callback method to render the view that will be unveiled on swipe + + /** + * Callback method to render the view that will be unveiled on swipe + */ renderQuickActions: renderItemType, }; type Props = SwipableListProps & FlatListProps; -type State = { +type State = {| openRowKey: ?string, -}; +|}; /** * A container component that renders multiple SwipeableRow's in a FlatList @@ -53,29 +60,9 @@ type State = { */ class SwipeableFlatList extends React.Component, State> { - props: Props; - state: State; - _flatListRef: ?FlatList = null; _shouldBounceFirstRowOnMount: boolean = false; - static propTypes = { - ...FlatList.propTypes, - - /** - * To alert the user that swiping is possible, the first row can bounce - * on component mount. - */ - bounceFirstRowOnMount: PropTypes.bool.isRequired, - - // Maximum distance to open to after a swipe - maxSwipeDistance: PropTypes.oneOfType([PropTypes.number, PropTypes.func]) - .isRequired, - - // Callback method to render the view that will be unveiled on swipe - renderQuickActions: PropTypes.func.isRequired, - }; - static defaultProps = { ...FlatList.defaultProps, bounceFirstRowOnMount: true, @@ -100,6 +87,7 @@ class SwipeableFlatList extends React.Component, State> { }} onScroll={this._onScroll} renderItem={this._renderItem} + extraData={this.state} /> ); } diff --git a/Libraries/Experimental/SwipeableRow/SwipeableListView.js b/Libraries/Experimental/SwipeableRow/SwipeableListView.js index bae4eb5f2ff846..3189b728a7962a 100644 --- a/Libraries/Experimental/SwipeableRow/SwipeableListView.js +++ b/Libraries/Experimental/SwipeableRow/SwipeableListView.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -11,30 +11,53 @@ 'use strict'; const ListView = require('ListView'); -const PropTypes = require('prop-types'); const React = require('React'); const SwipeableListViewDataSource = require('SwipeableListViewDataSource'); const SwipeableRow = require('SwipeableRow'); -type DefaultProps = { - bounceFirstRowOnMount: boolean, - renderQuickActions: Function, -}; +type ListViewProps = React.ElementConfig; + +type Props = $ReadOnly<{| + ...ListViewProps, -type Props = { + /** + * To alert the user that swiping is possible, the first row can bounce + * on component mount. + */ bounceFirstRowOnMount: boolean, + /** + * Use `SwipeableListView.getNewDataSource()` to get a data source to use, + * then use it just like you would a normal ListView data source + */ dataSource: SwipeableListViewDataSource, + /** + * Maximum distance to open to after a swipe + */ maxSwipeDistance: | number - | ((rowData: any, sectionID: string, rowID: string) => number), + | ((rowData: Object, sectionID: string, rowID: string) => number), onScroll?: ?Function, - renderRow: Function, - renderQuickActions: Function, -}; + /** + * Callback method to render the swipeable view + */ + renderRow: ( + rowData: Object, + sectionID: string, + rowID: string, + ) => React.Element, + /** + * Callback method to render the view that will be unveiled on swipe + */ + renderQuickActions: ( + rowData: Object, + sectionID: string, + rowID: string, + ) => ?React.Element, +|}>; -type State = { +type State = {| dataSource: Object, -}; +|}; /** * A container component that renders multiple SwipeableRow's in a ListView @@ -70,26 +93,6 @@ class SwipeableListView extends React.Component { }); } - static propTypes = { - /** - * To alert the user that swiping is possible, the first row can bounce - * on component mount. - */ - bounceFirstRowOnMount: PropTypes.bool.isRequired, - /** - * Use `SwipeableListView.getNewDataSource()` to get a data source to use, - * then use it just like you would a normal ListView data source - */ - dataSource: PropTypes.instanceOf(SwipeableListViewDataSource).isRequired, - // Maximum distance to open to after a swipe - maxSwipeDistance: PropTypes.oneOfType([PropTypes.number, PropTypes.func]) - .isRequired, - // Callback method to render the swipeable view - renderRow: PropTypes.func.isRequired, - // Callback method to render the view that will be unveiled on swipe - renderQuickActions: PropTypes.func.isRequired, - }; - static defaultProps = { bounceFirstRowOnMount: false, renderQuickActions: () => null, diff --git a/Libraries/Experimental/SwipeableRow/SwipeableListViewDataSource.js b/Libraries/Experimental/SwipeableRow/SwipeableListViewDataSource.js index 6c409a9acad1c4..38e03c66140d0f 100644 --- a/Libraries/Experimental/SwipeableRow/SwipeableListViewDataSource.js +++ b/Libraries/Experimental/SwipeableRow/SwipeableListViewDataSource.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/Experimental/SwipeableRow/SwipeableQuickActionButton.js b/Libraries/Experimental/SwipeableRow/SwipeableQuickActionButton.js index e0ea2ce8cabd23..0ad96e4d226afc 100644 --- a/Libraries/Experimental/SwipeableRow/SwipeableQuickActionButton.js +++ b/Libraries/Experimental/SwipeableRow/SwipeableQuickActionButton.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -10,12 +10,12 @@ 'use strict'; +const DeprecatedViewPropTypes = require('DeprecatedViewPropTypes'); const Image = require('Image'); const React = require('React'); const Text = require('Text'); const TouchableHighlight = require('TouchableHighlight'); const View = require('View'); -const ViewPropTypes = require('ViewPropTypes'); import type {ImageSource} from 'ImageSource'; @@ -27,13 +27,26 @@ import type {ImageSource} from 'ImageSource'; class SwipeableQuickActionButton extends React.Component<{ accessibilityLabel?: string, imageSource?: ?(ImageSource | number), - imageStyle?: ?ViewPropTypes.style, + /* $FlowFixMe(>=0.82.0 site=react_native_fb) This comment suppresses an error + * found when Flow v0.82 was deployed. To see the error delete this comment + * and run Flow. */ + imageStyle?: ?DeprecatedViewPropTypes.style, mainView?: ?React.Node, onPress?: Function, - style?: ?ViewPropTypes.style, + /* $FlowFixMe(>=0.82.0 site=react_native_fb) This comment suppresses an error + * found when Flow v0.82 was deployed. To see the error delete this comment + * and run Flow. */ + style?: ?DeprecatedViewPropTypes.style, + /* $FlowFixMe(>=0.82.0 site=react_native_fb) This comment suppresses an error + * found when Flow v0.82 was deployed. To see the error delete this comment + * and run Flow. */ + containerStyle?: ?DeprecatedViewPropTypes.style, testID?: string, text?: ?(string | Object | Array), - textStyle?: ?ViewPropTypes.style, + /* $FlowFixMe(>=0.82.0 site=react_native_fb) This comment suppresses an error + * found when Flow v0.82 was deployed. To see the error delete this comment + * and run Flow. */ + textStyle?: ?DeprecatedViewPropTypes.style, }> { render(): React.Node { if (!this.props.imageSource && !this.props.text && !this.props.mainView) { @@ -55,8 +68,9 @@ class SwipeableQuickActionButton extends React.Component<{ - {mainView} + underlayColor="transparent" + style={this.props.containerStyle}> + {mainView} ); } diff --git a/Libraries/Experimental/SwipeableRow/SwipeableQuickActions.js b/Libraries/Experimental/SwipeableRow/SwipeableQuickActions.js index 3642b135d43191..c97f77648af76f 100644 --- a/Libraries/Experimental/SwipeableRow/SwipeableQuickActions.js +++ b/Libraries/Experimental/SwipeableRow/SwipeableQuickActions.js @@ -1,11 +1,11 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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 + * @flow strict-local */ 'use strict'; @@ -14,7 +14,12 @@ const React = require('React'); const StyleSheet = require('StyleSheet'); const View = require('View'); -const ViewPropTypes = require('ViewPropTypes'); +import type {ViewStyleProp} from 'StyleSheet'; + +type Props = $ReadOnly<{| + style?: ?ViewStyleProp, + children: React.Node, +|}>; /** * A thin wrapper around standard quick action buttons that can, if the user @@ -26,13 +31,8 @@ const ViewPropTypes = require('ViewPropTypes'); * * */ -class SwipeableQuickActions extends React.Component<{style?: $FlowFixMe}> { - static propTypes = { - style: ViewPropTypes.style, - }; - +class SwipeableQuickActions extends React.Component { render(): React.Node { - // $FlowFixMe found when converting React.createClass to ES6 const children = this.props.children; let buttons = []; @@ -41,8 +41,7 @@ class SwipeableQuickActions extends React.Component<{style?: $FlowFixMe}> { for (let i = 0; i < children.length; i++) { buttons.push(children[i]); - // $FlowFixMe found when converting React.createClass to ES6 - if (i < this.props.children.length - 1) { + if (i < children.length - 1) { // Not last button buttons.push(); } diff --git a/Libraries/Experimental/SwipeableRow/SwipeableRow.js b/Libraries/Experimental/SwipeableRow/SwipeableRow.js index 3cbeb3d6ca2558..de707a3549d2f8 100644 --- a/Libraries/Experimental/SwipeableRow/SwipeableRow.js +++ b/Libraries/Experimental/SwipeableRow/SwipeableRow.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -14,16 +14,11 @@ const Animated = require('Animated'); const I18nManager = require('I18nManager'); const PanResponder = require('PanResponder'); const React = require('React'); -const PropTypes = require('prop-types'); const StyleSheet = require('StyleSheet'); -/* $FlowFixMe(>=0.54.0 site=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. */ -const TimerMixin = require('react-timer-mixin'); const View = require('View'); -const createReactClass = require('create-react-class'); -const emptyFunction = require('fbjs/lib/emptyFunction'); +import type {LayoutEvent, PressEvent} from 'CoreEventTypes'; +import type {GestureState} from 'PanResponder'; const IS_RTL = I18nManager.isRTL; @@ -56,21 +51,30 @@ const RIGHT_SWIPE_BOUNCE_BACK_DURATION = 300; * how far the finger swipes, and not the actual animation distance. */ const RIGHT_SWIPE_THRESHOLD = 30 * SLOW_SPEED_SWIPE_FACTOR; +const DEFAULT_SWIPE_THRESHOLD = 30; + +const emptyFunction = () => {}; type Props = $ReadOnly<{| children?: ?React.Node, isOpen?: ?boolean, maxSwipeDistance?: ?number, - onClose?: ?Function, - onOpen?: ?Function, - onSwipeEnd?: ?Function, - onSwipeStart?: ?Function, + onClose?: ?() => void, + onOpen?: ?() => void, + onSwipeEnd?: ?() => void, + onSwipeStart?: ?() => void, preventSwipeRight?: ?boolean, shouldBounceOnMount?: ?boolean, slideoutView?: ?React.Node, swipeThreshold?: ?number, |}>; +type State = { + currentLeft: Animated.Value, + isSwipeableViewRendered: boolean, + rowHeight: ?number, +}; + /** * Creates a swipable row that allows taps on the main item and a custom View * on the item hidden behind the row. Typically this should be used in @@ -78,75 +82,98 @@ type Props = $ReadOnly<{| * used in a normal ListView. See the renderRow for SwipeableListView to see how * to use this component separately. */ -const SwipeableRow = createReactClass({ - displayName: 'SwipeableRow', - _panResponder: {}, - _previousLeft: CLOSED_LEFT_POSITION, - - mixins: [TimerMixin], - - propTypes: { - children: PropTypes.any, - isOpen: PropTypes.bool, - preventSwipeRight: PropTypes.bool, - maxSwipeDistance: PropTypes.number.isRequired, - onOpen: PropTypes.func.isRequired, - onClose: PropTypes.func.isRequired, - onSwipeEnd: PropTypes.func.isRequired, - onSwipeStart: PropTypes.func.isRequired, - // Should bounce the row on mount - shouldBounceOnMount: PropTypes.bool, - /** - * A ReactElement that is unveiled when the user swipes - */ - slideoutView: PropTypes.node.isRequired, - /** - * The minimum swipe distance required before fully animating the swipe. If - * the user swipes less than this distance, the item will return to its - * previous (open/close) position. - */ - swipeThreshold: PropTypes.number.isRequired, - }, +class SwipeableRow extends React.Component { + _handleMoveShouldSetPanResponderCapture = ( + event: PressEvent, + gestureState: GestureState, + ): boolean => { + // Decides whether a swipe is responded to by this component or its child + return gestureState.dy < 10 && this._isValidSwipe(gestureState); + }; - getInitialState(): Object { - return { - currentLeft: new Animated.Value(this._previousLeft), - /** - * In order to render component A beneath component B, A must be rendered - * before B. However, this will cause "flickering", aka we see A briefly - * then B. To counter this, _isSwipeableViewRendered flag is used to set - * component A to be transparent until component B is loaded. - */ - isSwipeableViewRendered: false, - rowHeight: (null: ?number), - }; - }, + _handlePanResponderGrant = ( + event: PressEvent, + gestureState: GestureState, + ): void => {}; - getDefaultProps(): Object { - return { - isOpen: false, - preventSwipeRight: false, - maxSwipeDistance: 0, - onOpen: emptyFunction, - onClose: emptyFunction, - onSwipeEnd: emptyFunction, - onSwipeStart: emptyFunction, - swipeThreshold: 30, - }; - }, + _handlePanResponderMove = ( + event: PressEvent, + gestureState: GestureState, + ): void => { + if (this._isSwipingExcessivelyRightFromClosedPosition(gestureState)) { + return; + } - UNSAFE_componentWillMount(): void { - this._panResponder = PanResponder.create({ - onMoveShouldSetPanResponderCapture: this - ._handleMoveShouldSetPanResponderCapture, - onPanResponderGrant: this._handlePanResponderGrant, - onPanResponderMove: this._handlePanResponderMove, - onPanResponderRelease: this._handlePanResponderEnd, - onPanResponderTerminationRequest: this._onPanResponderTerminationRequest, - onPanResponderTerminate: this._handlePanResponderEnd, - onShouldBlockNativeResponder: (event, gestureState) => false, - }); - }, + this.props.onSwipeStart && this.props.onSwipeStart(); + + if (this._isSwipingRightFromClosed(gestureState)) { + this._swipeSlowSpeed(gestureState); + } else { + this._swipeFullSpeed(gestureState); + } + }; + + _onPanResponderTerminationRequest = ( + event: PressEvent, + gestureState: GestureState, + ): boolean => { + return false; + }; + + _handlePanResponderEnd = ( + event: PressEvent, + gestureState: GestureState, + ): void => { + const horizontalDistance = IS_RTL ? -gestureState.dx : gestureState.dx; + if (this._isSwipingRightFromClosed(gestureState)) { + this.props.onOpen && this.props.onOpen(); + this._animateBounceBack(RIGHT_SWIPE_BOUNCE_BACK_DURATION); + } else if (this._shouldAnimateRemainder(gestureState)) { + if (horizontalDistance < 0) { + // Swiped left + this.props.onOpen && this.props.onOpen(); + this._animateToOpenPositionWith(gestureState.vx, horizontalDistance); + } else { + // Swiped right + this.props.onClose && this.props.onClose(); + this._animateToClosedPosition(); + } + } else { + if (this._previousLeft === CLOSED_LEFT_POSITION) { + this._animateToClosedPosition(); + } else { + this._animateToOpenPosition(); + } + } + + this.props.onSwipeEnd && this.props.onSwipeEnd(); + }; + + _panResponder = PanResponder.create({ + onMoveShouldSetPanResponderCapture: this + ._handleMoveShouldSetPanResponderCapture, + onPanResponderGrant: this._handlePanResponderGrant, + onPanResponderMove: this._handlePanResponderMove, + onPanResponderRelease: this._handlePanResponderEnd, + onPanResponderTerminationRequest: this._onPanResponderTerminationRequest, + onPanResponderTerminate: this._handlePanResponderEnd, + onShouldBlockNativeResponder: (event, gestureState) => false, + }); + + _previousLeft = CLOSED_LEFT_POSITION; + _timeoutID: ?TimeoutID = null; + + state = { + currentLeft: new Animated.Value(this._previousLeft), + /** + * In order to render component A beneath component B, A must be rendered + * before B. However, this will cause "flickering", aka we see A briefly + * then B. To counter this, _isSwipeableViewRendered flag is used to set + * component A to be transparent until component B is loaded. + */ + isSwipeableViewRendered: false, + rowHeight: null, + }; componentDidMount(): void { if (this.props.shouldBounceOnMount) { @@ -154,21 +181,30 @@ const SwipeableRow = createReactClass({ * Do the on mount bounce after a delay because if we animate when other * components are loading, the animation will be laggy */ - this.setTimeout(() => { + this._timeoutID = setTimeout(() => { this._animateBounceBack(ON_MOUNT_BOUNCE_DURATION); }, ON_MOUNT_BOUNCE_DELAY); } - }, + } - UNSAFE_componentWillReceiveProps(nextProps: Object): void { + UNSAFE_componentWillReceiveProps(nextProps: $Shape): void { /** * We do not need an "animateOpen(noCallback)" because this animation is * handled internally by this component. */ - if (this.props.isOpen && !nextProps.isOpen) { + const isOpen = this.props.isOpen ?? false; + const nextIsOpen = nextProps.isOpen ?? false; + + if (isOpen && !nextIsOpen) { this._animateToClosedPosition(); } - }, + } + + componentWillUnmount() { + if (this._timeoutID != null) { + clearTimeout(this._timeoutID); + } + } render(): React.Element { // The view hidden behind the main view @@ -197,60 +233,38 @@ const SwipeableRow = createReactClass({ {swipeableView} ); - }, + } close(): void { - this.props.onClose(); + this.props.onClose && this.props.onClose(); this._animateToClosedPosition(); - }, + } - _onSwipeableViewLayout(event: Object): void { + _onSwipeableViewLayout = (event: LayoutEvent): void => { this.setState({ isSwipeableViewRendered: true, rowHeight: event.nativeEvent.layout.height, }); - }, + }; - _handleMoveShouldSetPanResponderCapture( - event: Object, - gestureState: Object, - ): boolean { - // Decides whether a swipe is responded to by this component or its child - return gestureState.dy < 10 && this._isValidSwipe(gestureState); - }, - - _handlePanResponderGrant(event: Object, gestureState: Object): void {}, - - _handlePanResponderMove(event: Object, gestureState: Object): void { - if (this._isSwipingExcessivelyRightFromClosedPosition(gestureState)) { - return; - } - - this.props.onSwipeStart(); - - if (this._isSwipingRightFromClosed(gestureState)) { - this._swipeSlowSpeed(gestureState); - } else { - this._swipeFullSpeed(gestureState); - } - }, - - _isSwipingRightFromClosed(gestureState: Object): boolean { + _isSwipingRightFromClosed(gestureState: GestureState): boolean { const gestureStateDx = IS_RTL ? -gestureState.dx : gestureState.dx; return this._previousLeft === CLOSED_LEFT_POSITION && gestureStateDx > 0; - }, + } - _swipeFullSpeed(gestureState: Object): void { + _swipeFullSpeed(gestureState: GestureState): void { this.state.currentLeft.setValue(this._previousLeft + gestureState.dx); - }, + } - _swipeSlowSpeed(gestureState: Object): void { + _swipeSlowSpeed(gestureState: GestureState): void { this.state.currentLeft.setValue( this._previousLeft + gestureState.dx / SLOW_SPEED_SWIPE_FACTOR, ); - }, + } - _isSwipingExcessivelyRightFromClosedPosition(gestureState: Object): boolean { + _isSwipingExcessivelyRightFromClosedPosition( + gestureState: GestureState, + ): boolean { /** * We want to allow a BIT of right swipe, to allow users to know that * swiping is available, but swiping right does not do anything @@ -261,14 +275,7 @@ const SwipeableRow = createReactClass({ this._isSwipingRightFromClosed(gestureState) && gestureStateDx > RIGHT_SWIPE_THRESHOLD ); - }, - - _onPanResponderTerminationRequest( - event: Object, - gestureState: Object, - ): boolean { - return false; - }, + } _animateTo( toValue: number, @@ -283,14 +290,15 @@ const SwipeableRow = createReactClass({ this._previousLeft = toValue; callback(); }); - }, + } _animateToOpenPosition(): void { - const maxSwipeDistance = IS_RTL - ? -this.props.maxSwipeDistance - : this.props.maxSwipeDistance; - this._animateTo(-maxSwipeDistance); - }, + const maxSwipeDistance = this.props.maxSwipeDistance ?? 0; + const directionAwareMaxSwipeDistance = IS_RTL + ? -maxSwipeDistance + : maxSwipeDistance; + this._animateTo(-directionAwareMaxSwipeDistance); + } _animateToOpenPositionWith(speed: number, distMoved: number): void { /** @@ -301,26 +309,25 @@ const SwipeableRow = createReactClass({ speed > HORIZONTAL_FULL_SWIPE_SPEED_THRESHOLD ? speed : HORIZONTAL_FULL_SWIPE_SPEED_THRESHOLD; + const maxSwipeDistance = this.props.maxSwipeDistance ?? 0; /** * Calculate the duration the row should take to swipe the remaining distance * at the same speed the user swiped (or the speed threshold) */ - const duration = Math.abs( - (this.props.maxSwipeDistance - Math.abs(distMoved)) / speed, - ); - const maxSwipeDistance = IS_RTL - ? -this.props.maxSwipeDistance - : this.props.maxSwipeDistance; - this._animateTo(-maxSwipeDistance, duration); - }, + const duration = Math.abs((maxSwipeDistance - Math.abs(distMoved)) / speed); + const directionAwareMaxSwipeDistance = IS_RTL + ? -maxSwipeDistance + : maxSwipeDistance; + this._animateTo(-directionAwareMaxSwipeDistance, duration); + } _animateToClosedPosition(duration: number = SWIPE_DURATION): void { this._animateTo(CLOSED_LEFT_POSITION, duration); - }, + } - _animateToClosedPositionDuringBounce(): void { + _animateToClosedPositionDuringBounce = (): void => { this._animateToClosedPosition(RIGHT_SWIPE_BOUNCE_BACK_DURATION); - }, + }; _animateBounceBack(duration: number): void { /** @@ -335,12 +342,13 @@ const SwipeableRow = createReactClass({ duration, this._animateToClosedPositionDuringBounce, ); - }, + } // Ignore swipes due to user's finger moving slightly when tapping - _isValidSwipe(gestureState: Object): boolean { + _isValidSwipe(gestureState: GestureState): boolean { + const preventSwipeRight = this.props.preventSwipeRight ?? false; if ( - this.props.preventSwipeRight && + preventSwipeRight && this._previousLeft === CLOSED_LEFT_POSITION && gestureState.dx > 0 ) { @@ -348,49 +356,19 @@ const SwipeableRow = createReactClass({ } return Math.abs(gestureState.dx) > HORIZONTAL_SWIPE_DISTANCE_THRESHOLD; - }, + } - _shouldAnimateRemainder(gestureState: Object): boolean { + _shouldAnimateRemainder(gestureState: GestureState): boolean { /** * If user has swiped past a certain distance, animate the rest of the way * if they let go */ + const swipeThreshold = this.props.swipeThreshold ?? DEFAULT_SWIPE_THRESHOLD; return ( - Math.abs(gestureState.dx) > this.props.swipeThreshold || + Math.abs(gestureState.dx) > swipeThreshold || gestureState.vx > HORIZONTAL_FULL_SWIPE_SPEED_THRESHOLD ); - }, - - _handlePanResponderEnd(event: Object, gestureState: Object): void { - const horizontalDistance = IS_RTL ? -gestureState.dx : gestureState.dx; - if (this._isSwipingRightFromClosed(gestureState)) { - this.props.onOpen(); - this._animateBounceBack(RIGHT_SWIPE_BOUNCE_BACK_DURATION); - } else if (this._shouldAnimateRemainder(gestureState)) { - if (horizontalDistance < 0) { - // Swiped left - this.props.onOpen(); - this._animateToOpenPositionWith(gestureState.vx, horizontalDistance); - } else { - // Swiped right - this.props.onClose(); - this._animateToClosedPosition(); - } - } else { - if (this._previousLeft === CLOSED_LEFT_POSITION) { - this._animateToClosedPosition(); - } else { - this._animateToOpenPosition(); - } - } - - this.props.onSwipeEnd(); - }, -}); - -// TODO: Delete this when `SwipeableRow` uses class syntax. -class TypedSwipeableRow extends React.Component { - close() {} + } } const styles = StyleSheet.create({ @@ -403,4 +381,4 @@ const styles = StyleSheet.create({ }, }); -module.exports = ((SwipeableRow: any): Class); +module.exports = SwipeableRow; diff --git a/Libraries/Experimental/WindowedListView.js b/Libraries/Experimental/WindowedListView.js index 15f1d0b4a8755a..4508f900d39c62 100644 --- a/Libraries/Experimental/WindowedListView.js +++ b/Libraries/Experimental/WindowedListView.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -14,7 +14,6 @@ const Batchinator = require('Batchinator'); const IncrementalGroup = require('IncrementalGroup'); const React = require('React'); const ScrollView = require('ScrollView'); -const Set = require('Set'); const StyleSheet = require('StyleSheet'); const Systrace = require('Systrace'); const View = require('View'); @@ -23,8 +22,8 @@ const ViewabilityHelper = require('ViewabilityHelper'); const clamp = require('clamp'); const deepDiffer = require('deepDiffer'); const infoLog = require('infoLog'); -const invariant = require('fbjs/lib/invariant'); -const nullthrows = require('fbjs/lib/nullthrows'); +const invariant = require('invariant'); +const nullthrows = require('nullthrows'); import type {NativeMethodsMixinType} from 'ReactNativeTypes'; @@ -778,11 +777,7 @@ class CellRenderer extends React.Component { if (DEBUG) { infoLog('render cell ' + this.props.rowIndex); const Text = require('Text'); - debug = ( - - Row: {this.props.rowIndex} - - ); + debug = Row: {this.props.rowIndex}; } const style = this._includeInLayoutLatch ? styles.include : styles.remove; return ( @@ -820,6 +815,9 @@ const styles = StyleSheet.create({ right: -removedXOffset, opacity: DEBUG ? 0.1 : 0, }, + debug: { + backgroundColor: 'lightblue', + }, }); module.exports = WindowedListView; diff --git a/Libraries/Geolocation/Geolocation.js b/Libraries/Geolocation/Geolocation.js index d252da95bca58f..943dc09f778b0e 100644 --- a/Libraries/Geolocation/Geolocation.js +++ b/Libraries/Geolocation/Geolocation.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -13,7 +13,7 @@ const NativeEventEmitter = require('NativeEventEmitter'); const RCTLocationObserver = require('NativeModules').LocationObserver; -const invariant = require('fbjs/lib/invariant'); +const invariant = require('invariant'); const logError = require('logError'); /* $FlowFixMe(>=0.54.0 site=react_native_oss) This comment suppresses an error * found when Flow v0.54 was deployed. To see the error delete this comment and diff --git a/Libraries/Geolocation/RCTLocationObserver.h b/Libraries/Geolocation/RCTLocationObserver.h index 5de0ea289d881d..f3f9e19b8f2902 100644 --- a/Libraries/Geolocation/RCTLocationObserver.h +++ b/Libraries/Geolocation/RCTLocationObserver.h @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/Geolocation/RCTLocationObserver.m b/Libraries/Geolocation/RCTLocationObserver.m index 9e15facfa7ef68..09cec2b3ff53e9 100644 --- a/Libraries/Geolocation/RCTLocationObserver.m +++ b/Libraries/Geolocation/RCTLocationObserver.m @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -353,11 +353,6 @@ - (void)locationManager:(CLLocationManager *)manager [_locationManager stopUpdatingLocation]; } - // Reset location accuracy if desiredAccuracy is changed. - // Otherwise update accuracy will force triggering didUpdateLocations, watchPosition would keeping receiving location updates, even there's no location changes. - if (ABS(_locationManager.desiredAccuracy - RCT_DEFAULT_LOCATION_ACCURACY) > 0.000001) { - _locationManager.desiredAccuracy = RCT_DEFAULT_LOCATION_ACCURACY; - } } - (void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error @@ -389,11 +384,6 @@ - (void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError * } [_pendingRequests removeAllObjects]; - // Reset location accuracy if desiredAccuracy is changed. - // Otherwise update accuracy will force triggering didUpdateLocations, watchPosition would keeping receiving location updates, even there's no location changes. - if (ABS(_locationManager.desiredAccuracy - RCT_DEFAULT_LOCATION_ACCURACY) > 0.000001) { - _locationManager.desiredAccuracy = RCT_DEFAULT_LOCATION_ACCURACY; - } } static void checkLocationConfig() diff --git a/Libraries/Image/AssetRegistry.js b/Libraries/Image/AssetRegistry.js index b050a9e239cd27..7d9626f8771f42 100644 --- a/Libraries/Image/AssetRegistry.js +++ b/Libraries/Image/AssetRegistry.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/Image/AssetSourceResolver.js b/Libraries/Image/AssetSourceResolver.js index a870a325242838..da2daa93968ac1 100644 --- a/Libraries/Image/AssetSourceResolver.js +++ b/Libraries/Image/AssetSourceResolver.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -22,8 +22,8 @@ import type {PackagerAsset} from 'AssetRegistry'; const PixelRatio = require('PixelRatio'); const Platform = require('Platform'); -const assetPathUtils = require('../../local-cli/bundle/assetPathUtils'); -const invariant = require('fbjs/lib/invariant'); +const assetPathUtils = require('./assetPathUtils'); +const invariant = require('invariant'); /** * Returns a path like 'assets/AwesomeModule/icon@2x.png' diff --git a/Libraries/Image/Image.android.js b/Libraries/Image/Image.android.js index 3869986e5f5872..a4269be70360a2 100644 --- a/Libraries/Image/Image.android.js +++ b/Libraries/Image/Image.android.js @@ -1,34 +1,33 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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 + * @flow * @format */ 'use strict'; -const ImageStylePropTypes = require('ImageStylePropTypes'); +const DeprecatedImageStylePropTypes = require('DeprecatedImageStylePropTypes'); +const DeprecatedStyleSheetPropType = require('DeprecatedStyleSheetPropType'); +const DeprecatedViewPropTypes = require('DeprecatedViewPropTypes'); +const ImageViewNativeComponent = require('ImageViewNativeComponent'); const NativeModules = require('NativeModules'); -const React = require('React'); -const ReactNative = require('ReactNative'); const PropTypes = require('prop-types'); +const React = require('React'); +const ReactNative = require('ReactNative'); // eslint-disable-line no-unused-vars const StyleSheet = require('StyleSheet'); -const StyleSheetPropType = require('StyleSheetPropType'); const TextAncestor = require('TextAncestor'); -const ViewPropTypes = require('ViewPropTypes'); const flattenStyle = require('flattenStyle'); const merge = require('merge'); -const requireNativeComponent = require('requireNativeComponent'); const resolveAssetSource = require('resolveAssetSource'); const {ImageLoader} = NativeModules; -const RKImage = requireNativeComponent('RCTImageView'); -const RCTTextInlineImage = requireNativeComponent('RCTTextInlineImage'); +const TextInlineImageNativeComponent = require('TextInlineImageNativeComponent'); import type {ImageProps as ImagePropsType} from 'ImageProps'; @@ -38,8 +37,8 @@ function generateRequestId() { } const ImageProps = { - ...ViewPropTypes, - style: StyleSheetPropType(ImageStylePropTypes), + ...DeprecatedViewPropTypes, + style: DeprecatedStyleSheetPropType(DeprecatedImageStylePropTypes), /** * See https://facebook.github.io/react-native/docs/image.html#source */ @@ -182,7 +181,7 @@ declare class ImageComponentType extends ReactNative.NativeComponent< */ let Image = ( props: ImagePropsType, - forwardedRef: ?React.Ref<'RCTTextInlineImage' | 'RKImage'>, + forwardedRef: ?React.Ref<'RCTTextInlineImage' | 'ImageViewNativeComponent'>, ) => { let source = resolveAssetSource(props.source); const defaultSource = resolveAssetSource(props.defaultSource); @@ -219,8 +218,12 @@ let Image = ( let style; let sources; if (source?.uri != null) { + /* $FlowFixMe(>=0.78.0 site=react_native_android_fb) This issue was found + * when making Flow check .android.js files. */ const {width, height} = source; style = flattenStyle([{width, height}, styles.base, props.style]); + /* $FlowFixMe(>=0.78.0 site=react_native_android_fb) This issue was found + * when making Flow check .android.js files. */ sources = [{uri: source.uri}]; } else { style = flattenStyle([styles.base, props.style]); @@ -232,6 +235,8 @@ let Image = ( style, shouldNotifyLoadEvents: !!(onLoadStart || onLoad || onLoadEnd || onError), src: sources, + /* $FlowFixMe(>=0.78.0 site=react_native_android_fb) This issue was found + * when making Flow check .android.js files. */ headers: source?.headers, defaultSrc: defaultSource ? defaultSource.uri : null, loadingIndicatorSrc: loadingIndicatorSource @@ -244,24 +249,26 @@ let Image = ( {hasTextAncestor => hasTextAncestor ? ( - + ) : ( - + ) } ); }; -// $FlowFixMe - TODO T29156721 `React.forwardRef` is not defined in Flow, yet. Image = React.forwardRef(Image); +Image.displayName = 'Image'; /** - * Prefetches a remote image for later use by downloading it to the disk - * cache + * Retrieve the width and height (in pixels) of an image prior to displaying it * - * See https://facebook.github.io/react-native/docs/image.html#prefetch + * See https://facebook.github.io/react-native/docs/image.html#getsize */ +/* $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. */ Image.getSize = getSize; /** @@ -270,6 +277,9 @@ Image.getSize = getSize; * * See https://facebook.github.io/react-native/docs/image.html#prefetch */ +/* $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. */ Image.prefetch = prefetch; /** @@ -277,6 +287,9 @@ Image.prefetch = prefetch; * * See https://facebook.github.io/react-native/docs/image.html#abortprefetch */ +/* $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. */ Image.abortPrefetch = abortPrefetch; /** @@ -284,6 +297,9 @@ Image.abortPrefetch = abortPrefetch; * * See https://facebook.github.io/react-native/docs/image.html#querycache */ +/* $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. */ Image.queryCache = queryCache; /** @@ -291,8 +307,14 @@ Image.queryCache = queryCache; * * See https://facebook.github.io/react-native/docs/image.html#resolveassetsource */ +/* $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. */ Image.resolveAssetSource = resolveAssetSource; +/* $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. */ Image.propTypes = ImageProps; const styles = StyleSheet.create({ @@ -301,4 +323,7 @@ const styles = StyleSheet.create({ }, }); +/* $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. */ module.exports = (Image: Class); diff --git a/Libraries/Image/Image.ios.js b/Libraries/Image/Image.ios.js index 82e651c0362924..7475caa0cf5b01 100644 --- a/Libraries/Image/Image.ios.js +++ b/Libraries/Image/Image.ios.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -9,10 +9,10 @@ */ 'use strict'; -const ImageProps = require('ImageProps'); +const DeprecatedImagePropType = require('DeprecatedImagePropType'); const NativeModules = require('NativeModules'); const React = require('React'); -const ReactNative = require('ReactNative'); +const ReactNative = require('ReactNative'); // eslint-disable-line no-unused-vars const StyleSheet = require('StyleSheet'); const flattenStyle = require('flattenStyle'); @@ -25,6 +25,8 @@ const RCTImageView = requireNativeComponent('RCTImageView'); import type {ImageProps as ImagePropsType} from 'ImageProps'; +import type {ImageStyleProp} from 'StyleSheet'; + function getSize( uri: string, success: (width: number, height: number) => void, @@ -44,13 +46,20 @@ function prefetch(url: string) { return ImageViewManager.prefetchImage(url); } +async function queryCache( + urls: Array, +): Promise> { + return await ImageViewManager.queryCache(urls); +} + declare class ImageComponentType extends ReactNative.NativeComponent< ImagePropsType, > { static getSize: typeof getSize; static prefetch: typeof prefetch; + static queryCache: typeof queryCache; static resolveAssetSource: typeof resolveAssetSource; - static propTypes: typeof ImageProps; + static propTypes: typeof DeprecatedImagePropType; } /** @@ -71,12 +80,14 @@ let Image = ( }; let sources; - let style; + let style: ImageStyleProp; if (Array.isArray(source)) { + // $FlowFixMe flattenStyle is not strong enough style = flattenStyle([styles.base, props.style]) || {}; sources = source; } else { const {width, height, uri} = source; + // $FlowFixMe flattenStyle is not strong enough style = flattenStyle([{width, height}, styles.base, props.style]) || {}; sources = [source]; @@ -112,14 +123,17 @@ let Image = ( ); }; -// $FlowFixMe - TODO T29156721 `React.forwardRef` is not defined in Flow, yet. Image = React.forwardRef(Image); +Image.displayName = 'Image'; /** * Retrieve the width and height (in pixels) of an image prior to displaying it. * * See https://facebook.github.io/react-native/docs/image.html#getsize */ +/* $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. */ Image.getSize = getSize; /** @@ -128,16 +142,35 @@ Image.getSize = getSize; * * See https://facebook.github.io/react-native/docs/image.html#prefetch */ +/* $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. */ Image.prefetch = prefetch; +/** + * Performs cache interrogation. + * + * See https://facebook.github.io/react-native/docs/image.html#querycache + */ +/* $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. */ +Image.queryCache = queryCache; + /** * Resolves an asset reference into an object. * * See https://facebook.github.io/react-native/docs/image.html#resolveassetsource */ +/* $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. */ Image.resolveAssetSource = resolveAssetSource; -Image.propTypes = ImageProps; +/* $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. */ +Image.propTypes = DeprecatedImagePropType; const styles = StyleSheet.create({ base: { @@ -145,4 +178,7 @@ const styles = StyleSheet.create({ }, }); +/* $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. */ module.exports = (Image: Class); diff --git a/Libraries/Image/ImageBackground.js b/Libraries/Image/ImageBackground.js index a128880a17807c..7d04576dcc0c92 100644 --- a/Libraries/Image/ImageBackground.js +++ b/Libraries/Image/ImageBackground.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/Image/ImageEditor.js b/Libraries/Image/ImageEditor.js index 65edcccf12204f..53785b680c15c1 100644 --- a/Libraries/Image/ImageEditor.js +++ b/Libraries/Image/ImageEditor.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -16,25 +16,25 @@ type ImageCropData = { * The top-left corner of the cropped image, specified in the original * image's coordinate space. */ - offset: { + offset: {| x: number, y: number, - }, + |}, /** * The size (dimensions) of the cropped image, specified in the original * image's coordinate space. */ - size: { + size: {| width: number, height: number, - }, + |}, /** * (Optional) size to scale the cropped image to. */ - displaySize?: ?{ + displaySize?: ?{| width: number, height: number, - }, + |}, /** * (Optional) the resizing mode to use when scaling the image. If the * `displaySize` param is not specified, this has no effect. diff --git a/Libraries/Image/ImageProps.js b/Libraries/Image/ImageProps.js index 54f6250e1f1458..99591c48f604b9 100644 --- a/Libraries/Image/ImageProps.js +++ b/Libraries/Image/ImageProps.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -10,35 +10,42 @@ 'use strict'; -const EdgeInsetsPropType = require('EdgeInsetsPropType'); -const ImageSourcePropType = require('ImageSourcePropType'); -const ImageStylePropTypes = require('ImageStylePropTypes'); -const PropTypes = require('prop-types'); -const StyleSheetPropType = require('StyleSheetPropType'); - +import type {SyntheticEvent, LayoutEvent} from 'CoreEventTypes'; +import type {EdgeInsetsProp} from 'EdgeInsetsPropType'; +import type {ImageSource} from 'ImageSource'; +import type {ViewStyleProp, ImageStyleProp} from 'StyleSheet'; import type {DimensionValue} from 'StyleSheetTypes'; import type {ViewProps} from 'ViewPropTypes'; -import type {ImageSource} from 'ImageSource'; -import type {EdgeInsetsProp} from 'EdgeInsetsPropType'; -import type {SyntheticEvent} from 'CoreEventTypes'; -import type {ImageStyleProp} from 'StyleSheet'; -type OnLoadEvent = SyntheticEvent< +export type ImageLoadEvent = SyntheticEvent< $ReadOnly<{| - // Only on Android - uri?: string, - source: $ReadOnly<{| width: number, height: number, url: string, |}>, + uri?: string, // Only on Android |}>, >; type IOSImageProps = $ReadOnly<{| + /** + * A static image to display while loading the image source. + * + * See https://facebook.github.io/react-native/docs/image.html#defaultsource + */ defaultSource?: ?ImageSource, + /** + * Invoked when a partial load of the image is complete. + * + * See https://facebook.github.io/react-native/docs/image.html#onpartialload + */ onPartialLoad?: ?() => void, + /** + * Invoked on download progress with `{nativeEvent: {loaded, total}}`. + * + * See https://facebook.github.io/react-native/docs/image.html#onprogress + */ onProgress?: ?( event: SyntheticEvent<$ReadOnly<{|loaded: number, total: number|}>>, ) => void, @@ -51,142 +58,111 @@ type AndroidImageProps = $ReadOnly<{| |}>; export type ImageProps = {| - ...ViewProps, + ...$Diff>, ...IOSImageProps, ...AndroidImageProps, - blurRadius?: number, - capInsets?: ?EdgeInsetsProp, - onError?: ?(event: SyntheticEvent<$ReadOnly<{||}>>) => void, - onLoad?: ?(event: OnLoadEvent) => void, - onLoadEnd?: ?() => void, - onLoadStart?: ?() => void, - resizeMethod?: ?('auto' | 'resize' | 'scale'), - source?: ?ImageSource, - style?: ImageStyleProp, - - // Can be set via props or style, for now - height?: DimensionValue, - width?: DimensionValue, - resizeMode?: ?('cover' | 'contain' | 'stretch' | 'repeat' | 'center'), - - src?: empty, - children?: empty, -|}; - -module.exports = { - /** - * See https://facebook.github.io/react-native/docs/image.html#style - */ - style: StyleSheetPropType(ImageStylePropTypes), - /** - * The image source (either a remote URL or a local file resource). - * - * See https://facebook.github.io/react-native/docs/image.html#source - */ - source: ImageSourcePropType, - /** - * A static image to display while loading the image source. - * - * See https://facebook.github.io/react-native/docs/image.html#defaultsource - */ - defaultSource: PropTypes.oneOfType([ - PropTypes.shape({ - uri: PropTypes.string, - width: PropTypes.number, - height: PropTypes.number, - scale: PropTypes.number, - }), - PropTypes.number, - ]), /** * When true, indicates the image is an accessibility element. * * See https://facebook.github.io/react-native/docs/image.html#accessible */ - accessible: PropTypes.bool, + accessible?: ?boolean, + /** * The text that's read by the screen reader when the user interacts with * the image. * * See https://facebook.github.io/react-native/docs/image.html#accessibilitylabel */ - accessibilityLabel: PropTypes.node, + accessibilityLabel?: ?Stringish, + /** * blurRadius: the blur radius of the blur filter added to the image * * See https://facebook.github.io/react-native/docs/image.html#blurradius */ - blurRadius: PropTypes.number, + blurRadius?: ?number, + /** * See https://facebook.github.io/react-native/docs/image.html#capinsets */ - capInsets: EdgeInsetsPropType, + capInsets?: ?EdgeInsetsProp, + /** - * See https://facebook.github.io/react-native/docs/image.html#resizemethod + * Invoked on load error with `{nativeEvent: {error}}`. + * + * See https://facebook.github.io/react-native/docs/image.html#onerror */ - resizeMethod: PropTypes.oneOf(['auto', 'resize', 'scale']), + onError?: ?(event: SyntheticEvent<$ReadOnly<{||}>>) => void, + /** - * Determines how to resize the image when the frame doesn't match the raw - * image dimensions. + * Invoked on mount and layout changes with + * `{nativeEvent: {layout: {x, y, width, height}}}`. * - * See https://facebook.github.io/react-native/docs/image.html#resizemode + * See https://facebook.github.io/react-native/docs/image.html#onlayout */ - resizeMode: PropTypes.oneOf([ - 'cover', - 'contain', - 'stretch', - 'repeat', - 'center', - ]), + + onLayout?: ?(event: LayoutEvent) => mixed, + /** - * A unique identifier for this element to be used in UI Automation - * testing scripts. + * Invoked when load completes successfully. * - * See https://facebook.github.io/react-native/docs/image.html#testid + * See https://facebook.github.io/react-native/docs/image.html#onload */ - testID: PropTypes.string, + onLoad?: ?(event: ImageLoadEvent) => void, + /** - * Invoked on mount and layout changes with - * `{nativeEvent: {layout: {x, y, width, height}}}`. + * Invoked when load either succeeds or fails. * - * See https://facebook.github.io/react-native/docs/image.html#onlayout + * See https://facebook.github.io/react-native/docs/image.html#onloadend */ - onLayout: PropTypes.func, + onLoadEnd?: ?() => void, + /** * Invoked on load start. * * See https://facebook.github.io/react-native/docs/image.html#onloadstart */ - onLoadStart: PropTypes.func, + onLoadStart?: ?() => void, + /** - * Invoked on download progress with `{nativeEvent: {loaded, total}}`. - * - * See https://facebook.github.io/react-native/docs/image.html#onprogress + * See https://facebook.github.io/react-native/docs/image.html#resizemethod */ - onProgress: PropTypes.func, + resizeMethod?: ?('auto' | 'resize' | 'scale'), + /** - * Invoked on load error with `{nativeEvent: {error}}`. + * The image source (either a remote URL or a local file resource). * - * See https://facebook.github.io/react-native/docs/image.html#onerror + * See https://facebook.github.io/react-native/docs/image.html#source */ - onError: PropTypes.func, + source?: ?ImageSource, + /** - * Invoked when a partial load of the image is complete. - * - * See https://facebook.github.io/react-native/docs/image.html#onpartialload + * See https://facebook.github.io/react-native/docs/image.html#style */ - onPartialLoad: PropTypes.func, + style?: ?ImageStyleProp, + + // Can be set via props or style, for now + height?: ?DimensionValue, + width?: ?DimensionValue, + /** - * Invoked when load completes successfully. + * Determines how to resize the image when the frame doesn't match the raw + * image dimensions. * - * See https://facebook.github.io/react-native/docs/image.html#onload + * See https://facebook.github.io/react-native/docs/image.html#resizemode */ - onLoad: PropTypes.func, + resizeMode?: ?('cover' | 'contain' | 'stretch' | 'repeat' | 'center'), + /** - * Invoked when load either succeeds or fails. + * A unique identifier for this element to be used in UI Automation + * testing scripts. * - * See https://facebook.github.io/react-native/docs/image.html#onloadend + * See https://facebook.github.io/react-native/docs/image.html#testid */ - onLoadEnd: PropTypes.func, -}; + testID?: ?string, + + src?: empty, + children?: empty, +|}; diff --git a/Libraries/Image/ImageResizeMode.js b/Libraries/Image/ImageResizeMode.js index 48779e383f3119..1dad3309b1588d 100644 --- a/Libraries/Image/ImageResizeMode.js +++ b/Libraries/Image/ImageResizeMode.js @@ -1,52 +1,36 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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 + * @flow strict * @format */ -'use strict'; -/* $FlowFixMe(>=0.54.0 site=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. */ -const keyMirror = require('fbjs/lib/keyMirror'); +'use strict'; /** - * ImageResizeMode - Enum for different image resizing modes, set via - * `resizeMode` style property on `` components. + * ImageResizeMode defines valid values for different image resizing modes set + * via the `resizeMode` style property on ``. */ -const ImageResizeMode = keyMirror({ - /** - * contain - The image will be resized such that it will be completely - * visible, contained within the frame of the View. - */ - contain: null, - /** - * cover - The image will be resized such that the entire area of the view - * is covered by the image, potentially clipping parts of the image. - */ - cover: null, - /** - * stretch - The image will be stretched to fill the entire frame of the - * view without clipping. This may change the aspect ratio of the image, - * distorting it. - */ - stretch: null, - /** - * center - The image will be scaled down such that it is completely visible, - * if bigger than the area of the view. - * The image will not be scaled up. - */ - center: null, +export type ImageResizeMode = + // Resize by scaling down such that it is completely visible, if bigger than + // the area of the view. The image will not be scaled up. + | 'center' + + // Resize such that it will be completely visible, contained within the frame + // of the View. + | 'contain' + + // Resize such that the entire area of the view is covered by the image, + // potentially clipping parts of the image. + | 'cover' - /** - * repeat - The image will be repeated to cover the frame of the View. The - * image will keep it's size and aspect ratio. - */ - repeat: null, -}); + // Resize by repeating to cover the frame of the View. The image will keep its + // size and aspect ratio. + | 'repeat' -module.exports = ImageResizeMode; + // Resize by stretching it to fill the entire frame of the view without + // clipping. This may change the aspect ratio of the image, distorting it. + | 'stretch'; diff --git a/Libraries/Image/ImageSource.js b/Libraries/Image/ImageSource.js index 5c3c84df901209..0e03dc78c0f5d1 100644 --- a/Libraries/Image/ImageSource.js +++ b/Libraries/Image/ImageSource.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -14,15 +14,73 @@ // that might have more keys. This also has to be inexact to support taking // instances of classes like FBIcon. // https://fburl.com/8lynhvtw -type ImageURISource = $ReadOnly<{ +export type ImageURISource = $ReadOnly<{ + /** + * `uri` is a string representing the resource identifier for the image, which + * could be an http address, a local file path, or the name of a static image + * resource (which should be wrapped in the `require('./path/to/image.png')` + * function). + */ uri?: ?string, + + /** + * `bundle` is the iOS asset bundle which the image is included in. This + * will default to [NSBundle mainBundle] if not set. + * @platform ios + */ bundle?: ?string, + + /** + * `method` is the HTTP Method to use. Defaults to GET if not specified. + */ method?: ?string, + + /** + * `headers` is an object representing the HTTP headers to send along with the + * request for a remote image. + */ headers?: ?Object, + + /** + * `body` is the HTTP body to send with the request. This must be a valid + * UTF-8 string, and will be sent exactly as specified, with no + * additional encoding (e.g. URL-escaping or base64) applied. + */ body?: ?string, + + /** + * `cache` determines how the requests handles potentially cached + * responses. + * + * - `default`: Use the native platforms default strategy. `useProtocolCachePolicy` on iOS. + * + * - `reload`: The data for the URL will be loaded from the originating source. + * No existing cache data should be used to satisfy a URL load request. + * + * - `force-cache`: The existing cached data will be used to satisfy the request, + * regardless of its age or expiration date. If there is no existing data in the cache + * corresponding the request, the data is loaded from the originating source. + * + * - `only-if-cached`: The existing cache data will be used to satisfy a request, regardless of + * its age or expiration date. If there is no existing data in the cache corresponding + * to a URL load request, no attempt is made to load the data from the originating source, + * and the load is considered to have failed. + * + * @platform ios + */ cache?: ?('default' | 'reload' | 'force-cache' | 'only-if-cached'), + + /** + * `width` and `height` can be specified if known at build time, in which case + * these will be used to set the default `` component dimensions. + */ width?: ?number, height?: ?number, + + /** + * `scale` is used to indicate the scale factor of the image. Defaults to 1.0 if + * unspecified, meaning that one image pixel equates to one display point / DIP. + */ scale?: ?number, }>; diff --git a/Libraries/Image/ImageSourcePropType.js b/Libraries/Image/ImageSourcePropType.js deleted file mode 100644 index 6f67113fe1bf18..00000000000000 --- a/Libraries/Image/ImageSourcePropType.js +++ /dev/null @@ -1,90 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @no-flow - * @format - */ -'use strict'; - -const PropTypes = require('prop-types'); - -const ImageURISourcePropType = PropTypes.shape({ - /** - * `uri` is a string representing the resource identifier for the image, which - * could be an http address, a local file path, or the name of a static image - * resource (which should be wrapped in the `require('./path/to/image.png')` - * function). - */ - uri: PropTypes.string, - /** - * `bundle` is the iOS asset bundle which the image is included in. This - * will default to [NSBundle mainBundle] if not set. - * @platform ios - */ - bundle: PropTypes.string, - /** - * `method` is the HTTP Method to use. Defaults to GET if not specified. - */ - method: PropTypes.string, - /** - * `headers` is an object representing the HTTP headers to send along with the - * request for a remote image. - */ - headers: PropTypes.objectOf(PropTypes.string), - /** - * `body` is the HTTP body to send with the request. This must be a valid - * UTF-8 string, and will be sent exactly as specified, with no - * additional encoding (e.g. URL-escaping or base64) applied. - */ - body: PropTypes.string, - /** - * `cache` determines how the requests handles potentially cached - * responses. - * - * - `default`: Use the native platforms default strategy. `useProtocolCachePolicy` on iOS. - * - * - `reload`: The data for the URL will be loaded from the originating source. - * No existing cache data should be used to satisfy a URL load request. - * - * - `force-cache`: The existing cached data will be used to satisfy the request, - * regardless of its age or expiration date. If there is no existing data in the cache - * corresponding the request, the data is loaded from the originating source. - * - * - `only-if-cached`: The existing cache data will be used to satisfy a request, regardless of - * its age or expiration date. If there is no existing data in the cache corresponding - * to a URL load request, no attempt is made to load the data from the originating source, - * and the load is considered to have failed. - * - * @platform ios - */ - cache: PropTypes.oneOf([ - 'default', - 'reload', - 'force-cache', - 'only-if-cached', - ]), - /** - * `width` and `height` can be specified if known at build time, in which case - * these will be used to set the default `` component dimensions. - */ - width: PropTypes.number, - height: PropTypes.number, - /** - * `scale` is used to indicate the scale factor of the image. Defaults to 1.0 if - * unspecified, meaning that one image pixel equates to one display point / DIP. - */ - scale: PropTypes.number, -}); - -const ImageSourcePropType = PropTypes.oneOfType([ - ImageURISourcePropType, - // Opaque type returned by require('./image.jpg') - PropTypes.number, - // Multiple sources - PropTypes.arrayOf(ImageURISourcePropType), -]); - -module.exports = ImageSourcePropType; diff --git a/Libraries/Image/ImageStore.js b/Libraries/Image/ImageStore.js index 5373020376a5f1..8ba5c3162be83d 100644 --- a/Libraries/Image/ImageStore.js +++ b/Libraries/Image/ImageStore.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -11,6 +11,19 @@ const RCTImageStoreManager = require('NativeModules').ImageStoreManager; +const Platform = require('Platform'); + +const warnOnce = require('warnOnce'); + +function warnUnimplementedMethod(methodName: string): void { + warnOnce( + `imagestore-${methodName}`, + `react-native: ImageStore.${methodName}() is not implemented on ${ + Platform.OS + }`, + ); +} + class ImageStore { /** * Check if the ImageStore contains image data for the specified URI. @@ -20,7 +33,7 @@ class ImageStore { if (RCTImageStoreManager.hasImageForTag) { RCTImageStoreManager.hasImageForTag(uri, callback); } else { - console.warn('hasImageForTag() not implemented'); + warnUnimplementedMethod('hasImageForTag'); } } @@ -36,7 +49,7 @@ class ImageStore { if (RCTImageStoreManager.removeImageForTag) { RCTImageStoreManager.removeImageForTag(uri); } else { - console.warn('removeImageForTag() not implemented'); + warnUnimplementedMethod('removeImageForTag'); } } @@ -56,7 +69,15 @@ class ImageStore { success: (uri: string) => void, failure: (error: any) => void, ) { - RCTImageStoreManager.addImageFromBase64(base64ImageData, success, failure); + if (RCTImageStoreManager.addImageFromBase64) { + RCTImageStoreManager.addImageFromBase64( + base64ImageData, + success, + failure, + ); + } else { + warnUnimplementedMethod('addImageFromBase64'); + } } /** @@ -75,7 +96,11 @@ class ImageStore { success: (base64ImageData: string) => void, failure: (error: any) => void, ) { - RCTImageStoreManager.getBase64ForTag(uri, success, failure); + if (RCTImageStoreManager.getBase64ForTag) { + RCTImageStoreManager.getBase64ForTag(uri, success, failure); + } else { + warnUnimplementedMethod('getBase64ForTag'); + } } } diff --git a/Libraries/Image/ImageStylePropTypes.js b/Libraries/Image/ImageStylePropTypes.js deleted file mode 100644 index 54e500994227db..00000000000000 --- a/Libraries/Image/ImageStylePropTypes.js +++ /dev/null @@ -1,62 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @flow - * @format - */ -'use strict'; - -const ColorPropType = require('ColorPropType'); -const ImageResizeMode = require('ImageResizeMode'); -const LayoutPropTypes = require('LayoutPropTypes'); -const ReactPropTypes = require('prop-types'); -const ShadowPropTypesIOS = require('ShadowPropTypesIOS'); -const TransformPropTypes = require('TransformPropTypes'); - -const ImageStylePropTypes = { - ...LayoutPropTypes, - ...ShadowPropTypesIOS, - ...TransformPropTypes, - resizeMode: ReactPropTypes.oneOf(Object.keys(ImageResizeMode)), - backfaceVisibility: ReactPropTypes.oneOf(['visible', 'hidden']), - backgroundColor: ColorPropType, - borderColor: ColorPropType, - borderWidth: ReactPropTypes.number, - borderRadius: ReactPropTypes.number, - overflow: ReactPropTypes.oneOf(['visible', 'hidden']), - - /** - * Changes the color of all the non-transparent pixels to the tintColor. - */ - tintColor: ColorPropType, - opacity: ReactPropTypes.number, - /** - * When the image has rounded corners, specifying an overlayColor will - * cause the remaining space in the corners to be filled with a solid color. - * This is useful in cases which are not supported by the Android - * implementation of rounded corners: - * - Certain resize modes, such as 'contain' - * - Animated GIFs - * - * A typical way to use this prop is with images displayed on a solid - * background and setting the `overlayColor` to the same color - * as the background. - * - * For details of how this works under the hood, see - * http://frescolib.org/docs/rounded-corners-and-circles.html - * - * @platform android - */ - overlayColor: ReactPropTypes.string, - - // Android-Specific styles - borderTopLeftRadius: ReactPropTypes.number, - borderTopRightRadius: ReactPropTypes.number, - borderBottomLeftRadius: ReactPropTypes.number, - borderBottomRightRadius: ReactPropTypes.number, -}; - -module.exports = ImageStylePropTypes; diff --git a/Libraries/Image/ImageViewNativeComponent.js b/Libraries/Image/ImageViewNativeComponent.js new file mode 100644 index 00000000000000..47f8d531f756d8 --- /dev/null +++ b/Libraries/Image/ImageViewNativeComponent.js @@ -0,0 +1,17 @@ +/** + * 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 requireNativeComponent = require('requireNativeComponent'); + +const ImageViewNativeComponent = requireNativeComponent('RCTImageView'); + +module.exports = ImageViewNativeComponent; diff --git a/Libraries/Image/RCTGIFImageDecoder.h b/Libraries/Image/RCTGIFImageDecoder.h index 1f942aef4c8ce6..f9cc0eef4ebf94 100644 --- a/Libraries/Image/RCTGIFImageDecoder.h +++ b/Libraries/Image/RCTGIFImageDecoder.h @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/Image/RCTGIFImageDecoder.m b/Libraries/Image/RCTGIFImageDecoder.m index 48ca8aefcf74b4..a0afd43f4da45a 100644 --- a/Libraries/Image/RCTGIFImageDecoder.m +++ b/Libraries/Image/RCTGIFImageDecoder.m @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -31,8 +31,22 @@ - (RCTImageLoaderCancellationBlock)decodeImageData:(NSData *)imageData completionHandler:(RCTImageLoaderCompletionBlock)completionHandler { CGImageSourceRef imageSource = CGImageSourceCreateWithData((CFDataRef)imageData, NULL); + if (!imageSource) { + completionHandler(nil, nil); + return ^{}; + } NSDictionary *properties = (__bridge_transfer NSDictionary *)CGImageSourceCopyProperties(imageSource, NULL); - NSUInteger loopCount = [properties[(id)kCGImagePropertyGIFDictionary][(id)kCGImagePropertyGIFLoopCount] unsignedIntegerValue]; + CGFloat loopCount = 0; + if ([[properties[(id)kCGImagePropertyGIFDictionary] allKeys] containsObject:(id)kCGImagePropertyGIFLoopCount]) { + loopCount = [properties[(id)kCGImagePropertyGIFDictionary][(id)kCGImagePropertyGIFLoopCount] unsignedIntegerValue]; + if (loopCount == 0) { + // A loop count of 0 means infinite + loopCount = HUGE_VALF; + } else { + // A loop count of 1 means it should repeat twice, 2 means, thrice, etc. + loopCount += 1; + } + } UIImage *image = nil; size_t imageCount = CGImageSourceGetCount(imageSource); @@ -44,6 +58,9 @@ - (RCTImageLoaderCancellationBlock)decodeImageData:(NSData *)imageData for (size_t i = 0; i < imageCount; i++) { CGImageRef imageRef = CGImageSourceCreateImageAtIndex(imageSource, i, NULL); + if (!imageRef) { + continue; + } if (!image) { image = [UIImage imageWithCGImage:imageRef scale:scale orientation:UIImageOrientationUp]; } @@ -54,10 +71,10 @@ - (RCTImageLoaderCancellationBlock)decodeImageData:(NSData *)imageData const NSTimeInterval kDelayTimeIntervalDefault = 0.1; NSNumber *delayTime = frameGIFProperties[(id)kCGImagePropertyGIFUnclampedDelayTime] ?: frameGIFProperties[(id)kCGImagePropertyGIFDelayTime]; if (delayTime == nil) { - if (i == 0) { + if (delays.count == 0) { delayTime = @(kDelayTimeIntervalDefault); } else { - delayTime = delays[i - 1]; + delayTime = delays.lastObject; } } @@ -67,8 +84,8 @@ - (RCTImageLoaderCancellationBlock)decodeImageData:(NSData *)imageData } duration += delayTime.doubleValue; - delays[i] = delayTime; - images[i] = (__bridge_transfer id)imageRef; + [delays addObject:delayTime]; + [images addObject:(__bridge_transfer id)imageRef]; } CFRelease(imageSource); @@ -84,11 +101,12 @@ - (RCTImageLoaderCancellationBlock)decodeImageData:(NSData *)imageData // Create animation CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"contents"]; animation.calculationMode = kCAAnimationDiscrete; - animation.repeatCount = loopCount == 0 ? HUGE_VALF : loopCount; + animation.repeatCount = loopCount; animation.keyTimes = keyTimes; animation.values = images; animation.duration = duration; animation.removedOnCompletion = NO; + animation.fillMode = kCAFillModeForwards; image.reactKeyframeAnimation = animation; } else { diff --git a/Libraries/Image/RCTImageBlurUtils.h b/Libraries/Image/RCTImageBlurUtils.h index 44df37441af92a..2ec7b358d240ee 100644 --- a/Libraries/Image/RCTImageBlurUtils.h +++ b/Libraries/Image/RCTImageBlurUtils.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2013-present, Facebook, Inc. + * 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. diff --git a/Libraries/Image/RCTImageBlurUtils.m b/Libraries/Image/RCTImageBlurUtils.m index 84c90019bfbcc9..8ee3282de4de4e 100644 --- a/Libraries/Image/RCTImageBlurUtils.m +++ b/Libraries/Image/RCTImageBlurUtils.m @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -35,6 +35,12 @@ size_t bytes = buffer1.rowBytes * buffer1.height; buffer1.data = malloc(bytes); buffer2.data = malloc(bytes); + if (!buffer1.data || !buffer2.data) { + // CWE - 391 : Unchecked error condition + // https://www.cvedetails.com/cwe-details/391/Unchecked-Error-Condition.html + // https://eli.thegreenplace.net/2009/10/30/handling-out-of-memory-conditions-in-c + abort(); + } // A description of how to compute the box kernel width from the Gaussian // radius (aka standard deviation) appears in the SVG spec: @@ -45,6 +51,12 @@ //create temp buffer void *tempBuffer = malloc((size_t)vImageBoxConvolve_ARGB8888(&buffer1, &buffer2, NULL, 0, 0, boxSize, boxSize, NULL, kvImageEdgeExtend + kvImageGetTempBufferSize)); + if (!tempBuffer) { + // CWE - 391 : Unchecked error condition + // https://www.cvedetails.com/cwe-details/391/Unchecked-Error-Condition.html + // https://eli.thegreenplace.net/2009/10/30/handling-out-of-memory-conditions-in-c + abort(); + } //copy image data CFDataRef dataSource = CGDataProviderCopyData(CGImageGetDataProvider(imageRef)); diff --git a/Libraries/Image/RCTImageCache.h b/Libraries/Image/RCTImageCache.h index c0d24ad25a2d41..b7acc1126ca6cd 100644 --- a/Libraries/Image/RCTImageCache.h +++ b/Libraries/Image/RCTImageCache.h @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/Image/RCTImageCache.m b/Libraries/Image/RCTImageCache.m index 4d4803c4ab08e0..a6546a7e26ce17 100644 --- a/Libraries/Image/RCTImageCache.m +++ b/Libraries/Image/RCTImageCache.m @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -18,35 +18,39 @@ #import "RCTImageUtils.h" -static const NSUInteger RCTMaxCachableDecodedImageSizeInBytes = 1048576; // 1MB +static const NSUInteger RCTMaxCachableDecodedImageSizeInBytes = 2097152; // 2 MB static NSString *RCTCacheKeyForImage(NSString *imageTag, CGSize size, CGFloat scale, - RCTResizeMode resizeMode, NSString *responseDate) + RCTResizeMode resizeMode) { - return [NSString stringWithFormat:@"%@|%g|%g|%g|%lld|%@", - imageTag, size.width, size.height, scale, (long long)resizeMode, responseDate]; + return [NSString stringWithFormat:@"%@|%g|%g|%g|%lld", + imageTag, size.width, size.height, scale, (long long)resizeMode]; } @implementation RCTImageCache { NSOperationQueue *_imageDecodeQueue; NSCache *_decodedImageCache; + NSMutableDictionary *_cacheStaleTimes; } - (instancetype)init { - _decodedImageCache = [NSCache new]; - _decodedImageCache.totalCostLimit = 5 * 1024 * 1024; // 5MB - - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(clearCache) - name:UIApplicationDidReceiveMemoryWarningNotification - object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(clearCache) - name:UIApplicationWillResignActiveNotification - object:nil]; - + if (self = [super init]) { + _decodedImageCache = [NSCache new]; + _decodedImageCache.totalCostLimit = 20 * 1024 * 1024; // 20 MB + _cacheStaleTimes = [[NSMutableDictionary alloc] init]; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(clearCache) + name:UIApplicationDidReceiveMemoryWarningNotification + object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(clearCache) + name:UIApplicationWillResignActiveNotification + object:nil]; + } + return self; } @@ -58,6 +62,9 @@ - (void)dealloc - (void)clearCache { [_decodedImageCache removeAllObjects]; + @synchronized(_cacheStaleTimes) { + [_cacheStaleTimes removeAllObjects]; + } } - (void)addImageToCache:(UIImage *)image @@ -66,7 +73,7 @@ - (void)addImageToCache:(UIImage *)image if (!image) { return; } - CGFloat bytes = image.size.width * image.size.height * image.scale * image.scale * 4; + NSInteger bytes = image.reactDecodedImageBytes; if (bytes <= RCTMaxCachableDecodedImageSizeInBytes) { [self->_decodedImageCache setObject:image forKey:cacheKey @@ -78,9 +85,19 @@ - (UIImage *)imageForUrl:(NSString *)url size:(CGSize)size scale:(CGFloat)scale resizeMode:(RCTResizeMode)resizeMode - responseDate:(NSString *)responseDate { - NSString *cacheKey = RCTCacheKeyForImage(url, size, scale, resizeMode, responseDate); + NSString *cacheKey = RCTCacheKeyForImage(url, size, scale, resizeMode); + @synchronized(_cacheStaleTimes) { + id staleTime = _cacheStaleTimes[cacheKey]; + if (staleTime) { + if ([[NSDate new] compare:(NSDate *)staleTime] == NSOrderedDescending) { + // cached image has expired, clear it out to make room for others + [_cacheStaleTimes removeObjectForKey:cacheKey]; + [_decodedImageCache removeObjectForKey:cacheKey]; + return nil; + } + } + } return [_decodedImageCache objectForKey:cacheKey]; } @@ -89,10 +106,63 @@ - (void)addImageToCache:(UIImage *)image size:(CGSize)size scale:(CGFloat)scale resizeMode:(RCTResizeMode)resizeMode - responseDate:(NSString *)responseDate + response:(NSURLResponse *)response { - NSString *cacheKey = RCTCacheKeyForImage(url, size, scale, resizeMode, responseDate); - return [self addImageToCache:image forKey:cacheKey]; + if ([response isKindOfClass:[NSHTTPURLResponse class]]) { + NSString *cacheKey = RCTCacheKeyForImage(url, size, scale, resizeMode); + BOOL shouldCache = YES; + NSString *responseDate = ((NSHTTPURLResponse *)response).allHeaderFields[@"Date"]; + NSDate *originalDate = [self dateWithHeaderString:responseDate]; + NSString *cacheControl = ((NSHTTPURLResponse *)response).allHeaderFields[@"Cache-Control"]; + NSDate *staleTime; + NSArray *components = [cacheControl componentsSeparatedByString:@","]; + for (NSString *component in components) { + if ([component containsString:@"no-cache"] || [component containsString:@"no-store"] || [component hasSuffix:@"max-age=0"]) { + shouldCache = NO; + break; + } else { + NSRange range = [component rangeOfString:@"max-age="]; + if (range.location != NSNotFound) { + NSInteger seconds = [[component substringFromIndex:range.location + range.length] integerValue]; + staleTime = [originalDate dateByAddingTimeInterval:(NSTimeInterval)seconds]; + } + } + } + if (shouldCache) { + if (!staleTime && originalDate) { + NSString *expires = ((NSHTTPURLResponse *)response).allHeaderFields[@"Expires"]; + NSString *lastModified = ((NSHTTPURLResponse *)response).allHeaderFields[@"Last-Modified"]; + if (expires) { + staleTime = [self dateWithHeaderString:expires]; + } else if (lastModified) { + NSDate *lastModifiedDate = [self dateWithHeaderString:lastModified]; + if (lastModifiedDate) { + NSTimeInterval interval = [originalDate timeIntervalSinceDate:lastModifiedDate] / 10; + staleTime = [originalDate dateByAddingTimeInterval:interval]; + } + } + } + if (staleTime) { + @synchronized(_cacheStaleTimes) { + _cacheStaleTimes[cacheKey] = staleTime; + } + } + return [self addImageToCache:image forKey:cacheKey]; + } + } +} + +- (NSDate *)dateWithHeaderString:(NSString *)headerDateString { + static NSDateFormatter *formatter; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + formatter = [[NSDateFormatter alloc] init]; + formatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]; + formatter.dateFormat = @"EEE',' dd MMM yyyy HH':'mm':'ss 'GMT'"; + formatter.timeZone = [NSTimeZone timeZoneForSecondsFromGMT:0]; + }); + + return [formatter dateFromString:headerDateString]; } @end diff --git a/Libraries/Image/RCTImageEditingManager.h b/Libraries/Image/RCTImageEditingManager.h index dda409af33fc22..684855f999e3b1 100644 --- a/Libraries/Image/RCTImageEditingManager.h +++ b/Libraries/Image/RCTImageEditingManager.h @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/Image/RCTImageEditingManager.m b/Libraries/Image/RCTImageEditingManager.m index 2c8330ee9a302b..eb87df96fa77b5 100644 --- a/Libraries/Image/RCTImageEditingManager.m +++ b/Libraries/Image/RCTImageEditingManager.m @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/Image/RCTImageLoader.h b/Libraries/Image/RCTImageLoader.h index a0c6e798a8a960..da4e04ced955d6 100644 --- a/Libraries/Image/RCTImageLoader.h +++ b/Libraries/Image/RCTImageLoader.h @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -24,15 +24,14 @@ typedef dispatch_block_t RCTImageLoaderCancellationBlock; - (UIImage *)imageForUrl:(NSString *)url size:(CGSize)size scale:(CGFloat)scale - resizeMode:(RCTResizeMode)resizeMode - responseDate:(NSString *)responseDate; + resizeMode:(RCTResizeMode)resizeMode; - (void)addImageToCache:(UIImage *)image URL:(NSString *)url size:(CGSize)size scale:(CGFloat)scale resizeMode:(RCTResizeMode)resizeMode - responseDate:(NSString *)responseDate; + response:(NSURLResponse *)response; @end @@ -52,6 +51,11 @@ typedef dispatch_block_t RCTImageLoaderCancellationBlock; @property (nonatomic, copy) CAKeyframeAnimation *reactKeyframeAnimation; +/** + * Memory bytes of the image with the default calculation of static image or GIF. Custom calculations of decoded bytes can be assigned manually. + */ +@property (nonatomic, assign) NSInteger reactDecodedImageBytes; + @end @interface RCTImageLoader : NSObject @@ -129,10 +133,18 @@ typedef dispatch_block_t RCTImageLoaderCancellationBlock; */ - (RCTImageLoaderCancellationBlock)getImageSizeForURLRequest:(NSURLRequest *)imageURLRequest block:(void(^)(NSError *error, CGSize size))completionBlock; +/** + * Determines whether given image URLs are cached locally. The `requests` array is expected + * to contain objects convertible to NSURLRequest. The return value maps URLs to strings: + * "disk" for images known to be cached in non-volatile storage, "memory" for images known + * to be cached in memory. Dictionary items corresponding to images that are not known to be + * cached are simply missing. + */ +- (NSDictionary *)getImageCacheStatus:(NSArray *)requests; /** * Allows developers to set their own caching implementation for - * decoded images as long as it conforms to the RCTImageCacheDelegate + * decoded images as long as it conforms to the RCTImageCache * protocol. This method should be called in bridgeDidInitializeModule. */ - (void)setImageCache:(id)cache; diff --git a/Libraries/Image/RCTImageLoader.m b/Libraries/Image/RCTImageLoader.m index 775e53e95f7a08..b9276f00f86eaf 100644 --- a/Libraries/Image/RCTImageLoader.m +++ b/Libraries/Image/RCTImageLoader.m @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -20,6 +20,17 @@ #import "RCTImageCache.h" #import "RCTImageUtils.h" +static NSInteger RCTImageBytesForImage(UIImage *image) +{ + CAKeyframeAnimation *keyFrameAnimation = [image reactKeyframeAnimation]; + NSInteger singleImageBytes = image.size.width * image.size.height * image.scale * image.scale * 4; + if (keyFrameAnimation) { + return keyFrameAnimation.values.count * singleImageBytes; + } else { + return image.images ? image.images.count * singleImageBytes : singleImageBytes; + } +} + @implementation UIImage (React) - (CAKeyframeAnimation *)reactKeyframeAnimation @@ -32,6 +43,20 @@ - (void)setReactKeyframeAnimation:(CAKeyframeAnimation *)reactKeyframeAnimation objc_setAssociatedObject(self, @selector(reactKeyframeAnimation), reactKeyframeAnimation, OBJC_ASSOCIATION_COPY_NONATOMIC); } +- (NSInteger)reactDecodedImageBytes +{ + NSNumber *imageBytes = objc_getAssociatedObject(self, _cmd); + if (!imageBytes) { + imageBytes = @(RCTImageBytesForImage(self)); + } + return [imageBytes integerValue]; +} + +- (void)setReactDecodedImageBytes:(NSInteger)bytes +{ + objc_setAssociatedObject(self, @selector(reactDecodedImageBytes), @(bytes), OBJC_ASSOCIATION_RETAIN_NONATOMIC); +} + @end @implementation RCTImageLoader @@ -236,7 +261,7 @@ - (void)setImageCache:(id)cache return image; } -- (RCTImageLoaderCancellationBlock)loadImageWithURLRequest:(NSURLRequest *)imageURLRequest +- (RCTImageLoaderCancellationBlock) loadImageWithURLRequest:(NSURLRequest *)imageURLRequest callback:(RCTImageLoaderCompletionBlock)callback { return [self loadImageWithURLRequest:imageURLRequest @@ -321,7 +346,7 @@ - (RCTImageLoaderCancellationBlock)_loadImageOrDataWithURLRequest:(NSURLRequest resizeMode:(RCTResizeMode)resizeMode progressBlock:(RCTImageLoaderProgressBlock)progressHandler partialLoadBlock:(RCTImageLoaderPartialLoadBlock)partialLoadHandler - completionBlock:(void (^)(NSError *error, id imageOrData, BOOL cacheResult, NSString *fetchDate))completionBlock + completionBlock:(void (^)(NSError *error, id imageOrData, BOOL cacheResult, NSURLResponse *response))completionBlock { { NSMutableURLRequest *mutableRequest = [request mutableCopy]; @@ -344,15 +369,15 @@ - (RCTImageLoaderCancellationBlock)_loadImageOrDataWithURLRequest:(NSURLRequest BOOL requiresScheduling = [loadHandler respondsToSelector:@selector(requiresScheduling)] ? [loadHandler requiresScheduling] : YES; + BOOL cacheResult = [loadHandler respondsToSelector:@selector(shouldCacheLoadedImages)] ? + [loadHandler shouldCacheLoadedImages] : YES; + __block atomic_bool cancelled = ATOMIC_VAR_INIT(NO); // TODO: Protect this variable shared between threads. __block dispatch_block_t cancelLoad = nil; - void (^completionHandler)(NSError *, id, NSString *) = ^(NSError *error, id imageOrData, NSString *fetchDate) { + void (^completionHandler)(NSError *, id, NSURLResponse *) = ^(NSError *error, id imageOrData, NSURLResponse *response) { cancelLoad = nil; - BOOL cacheResult = [loadHandler respondsToSelector:@selector(shouldCacheLoadedImages)] ? - [loadHandler shouldCacheLoadedImages] : YES; - // If we've received an image, we should try to set it synchronously, // if it's data, do decoding on a background thread. if (RCTIsMainQueue() && ![imageOrData isKindOfClass:[UIImage class]]) { @@ -360,11 +385,11 @@ - (RCTImageLoaderCancellationBlock)_loadImageOrDataWithURLRequest:(NSURLRequest // expecting it, and may do expensive post-processing in the callback dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ if (!atomic_load(&cancelled)) { - completionBlock(error, imageOrData, cacheResult, fetchDate); + completionBlock(error, imageOrData, cacheResult, response); } }); } else if (!atomic_load(&cancelled)) { - completionBlock(error, imageOrData, cacheResult, fetchDate); + completionBlock(error, imageOrData, cacheResult, response); } }; @@ -405,10 +430,22 @@ - (RCTImageLoaderCancellationBlock)_loadImageOrDataWithURLRequest:(NSURLRequest completionHandler(error, image, nil); }]; } else { - // Use networking module to load image - cancelLoad = [strongSelf _loadURLRequest:request - progressBlock:progressHandler - completionBlock:completionHandler]; + UIImage *image; + if (cacheResult) { + image = [[strongSelf imageCache] imageForUrl:request.URL.absoluteString + size:size + scale:scale + resizeMode:resizeMode]; + } + + if (image) { + completionHandler(nil, image, nil); + } else { + // Use networking module to load image + cancelLoad = [strongSelf _loadURLRequest:request + progressBlock:progressHandler + completionBlock:completionHandler]; + } } }); @@ -427,7 +464,7 @@ - (RCTImageLoaderCancellationBlock)_loadImageOrDataWithURLRequest:(NSURLRequest - (RCTImageLoaderCancellationBlock)_loadURLRequest:(NSURLRequest *)request progressBlock:(RCTImageLoaderProgressBlock)progressHandler - completionBlock:(void (^)(NSError *error, id imageOrData, NSString *fetchDate))completionHandler + completionBlock:(void (^)(NSError *error, id imageOrData, NSURLResponse *response))completionHandler { // Check if networking module is available if (RCT_DEBUG && ![_bridge respondsToSelector:@selector(networking)]) { @@ -449,18 +486,17 @@ - (RCTImageLoaderCancellationBlock)_loadURLRequest:(NSURLRequest *)request RCTURLRequestCompletionBlock processResponse = ^(NSURLResponse *response, NSData *data, NSError *error) { // Check for system errors if (error) { - completionHandler(error, nil, nil); + completionHandler(error, nil, response); return; } else if (!response) { - completionHandler(RCTErrorWithMessage(@"Response metadata error"), nil, nil); + completionHandler(RCTErrorWithMessage(@"Response metadata error"), nil, response); return; } else if (!data) { - completionHandler(RCTErrorWithMessage(@"Unknown image download error"), nil, nil); + completionHandler(RCTErrorWithMessage(@"Unknown image download error"), nil, response); return; } // Check for http errors - NSString *responseDate; if ([response isKindOfClass:[NSHTTPURLResponse class]]) { NSInteger statusCode = ((NSHTTPURLResponse *)response).statusCode; if (statusCode != 200) { @@ -468,15 +504,13 @@ - (RCTImageLoaderCancellationBlock)_loadURLRequest:(NSURLRequest *)request NSDictionary *userInfo = @{NSLocalizedDescriptionKey: errorMessage}; completionHandler([[NSError alloc] initWithDomain:NSURLErrorDomain code:statusCode - userInfo:userInfo], nil, nil); + userInfo:userInfo], nil, response); return; } - - responseDate = ((NSHTTPURLResponse *)response).allHeaderFields[@"Date"]; } // Call handler - completionHandler(nil, data, responseDate); + completionHandler(nil, data, response); }; // Download image @@ -498,7 +532,7 @@ - (RCTImageLoaderCancellationBlock)_loadURLRequest:(NSURLRequest *)request } else { someError = RCTErrorWithMessage(@"Unknown image download error"); } - completionHandler(someError, nil, nil); + completionHandler(someError, nil, response); [strongSelf dequeueTasks]; return; } @@ -564,7 +598,7 @@ - (RCTImageLoaderCancellationBlock)loadImageWithURLRequest:(NSURLRequest *)image }; __weak RCTImageLoader *weakSelf = self; - void (^completionHandler)(NSError *, id, BOOL, NSString *) = ^(NSError *error, id imageOrData, BOOL cacheResult, NSString *fetchDate) { + void (^completionHandler)(NSError *, id, BOOL, NSURLResponse *) = ^(NSError *error, id imageOrData, BOOL cacheResult, NSURLResponse *response) { __typeof(self) strongSelf = weakSelf; if (atomic_load(&cancelled) || !strongSelf) { return; @@ -576,20 +610,6 @@ - (RCTImageLoaderCancellationBlock)loadImageWithURLRequest:(NSURLRequest *)image return; } - // Check decoded image cache - if (cacheResult) { - UIImage *image = [[strongSelf imageCache] imageForUrl:imageURLRequest.URL.absoluteString - size:size - scale:scale - resizeMode:resizeMode - responseDate:fetchDate]; - if (image) { - cancelLoad = nil; - completionBlock(nil, image); - return; - } - } - RCTImageLoaderCompletionBlock decodeCompletionHandler = ^(NSError *error_, UIImage *image) { if (cacheResult && image) { // Store decoded image in cache @@ -598,7 +618,7 @@ - (RCTImageLoaderCancellationBlock)loadImageWithURLRequest:(NSURLRequest *)image size:size scale:scale resizeMode:resizeMode - responseDate:fetchDate]; + response:response]; } cancelLoad = nil; @@ -732,7 +752,7 @@ - (RCTImageLoaderCancellationBlock)decodeImageData:(NSData *)data - (RCTImageLoaderCancellationBlock)getImageSizeForURLRequest:(NSURLRequest *)imageURLRequest block:(void(^)(NSError *error, CGSize size))callback { - void (^completion)(NSError *, id, BOOL, NSString *) = ^(NSError *error, id imageOrData, BOOL cacheResult, NSString *fetchDate) { + void (^completion)(NSError *, id, BOOL, NSURLResponse *) = ^(NSError *error, id imageOrData, BOOL cacheResult, NSURLResponse *response) { CGSize size; if ([imageOrData isKindOfClass:[NSData class]]) { NSDictionary *meta = RCTGetImageMetadata(imageOrData); @@ -779,45 +799,69 @@ - (RCTImageLoaderCancellationBlock)getImageSizeForURLRequest:(NSURLRequest *)ima completionBlock:completion]; } -#pragma mark - RCTURLRequestHandler - -- (BOOL)canHandleRequest:(NSURLRequest *)request +- (NSDictionary *)getImageCacheStatus:(NSArray *)requests { - NSURL *requestURL = request.URL; - - // If the data being loaded is a video, return NO - // Even better may be to implement this on the RCTImageURLLoader that would try to load it, - // but we'd have to run the logic both in RCTPhotoLibraryImageLoader and - // RCTAssetsLibraryRequestHandler. Once we drop iOS7 though, we'd drop - // RCTAssetsLibraryRequestHandler and can move it there. - static NSRegularExpression *videoRegex = nil; - if (!videoRegex) { - NSError *error = nil; - videoRegex = [NSRegularExpression regularExpressionWithPattern:@"(?:&|^)ext=MOV(?:&|$)" - options:NSRegularExpressionCaseInsensitive - error:&error]; - if (error) { - RCTLogError(@"%@", error); + NSMutableDictionary *results = [NSMutableDictionary dictionary]; + for (id request in requests) { + NSURLRequest *urlRequest = [RCTConvert NSURLRequest:request]; + if (urlRequest) { + NSCachedURLResponse *cachedResponse = [NSURLCache.sharedURLCache cachedResponseForRequest:urlRequest]; + if (cachedResponse) { + if (cachedResponse.storagePolicy == NSURLCacheStorageAllowedInMemoryOnly) { + [results setObject:@"memory" forKey:urlRequest.URL.absoluteString]; + } else { + [results setObject:@"disk" forKey:urlRequest.URL.absoluteString]; + } } } + } + return results; +} - NSString *query = requestURL.query; - if (query != nil && [videoRegex firstMatchInString:query - options:0 - range:NSMakeRange(0, query.length)]) { - return NO; - } +#pragma mark - RCTURLRequestHandler - for (id loader in _loaders) { - // Don't use RCTImageURLLoader protocol for modules that already conform to - // RCTURLRequestHandler as it's inefficient to decode an image and then - // convert it back into data - if (![loader conformsToProtocol:@protocol(RCTURLRequestHandler)] && - [loader canLoadImageURL:requestURL]) { - return YES; - } +- (BOOL)canHandleRequest:(NSURLRequest *)request +{ + NSURL *requestURL = request.URL; + + // If the data being loaded is a video, return NO + // Even better may be to implement this on the RCTImageURLLoader that would try to load it, + // but we'd have to run the logic both in RCTPhotoLibraryImageLoader and + // RCTAssetsLibraryRequestHandler. Once we drop iOS7 though, we'd drop + // RCTAssetsLibraryRequestHandler and can move it there. + static NSRegularExpression *videoRegex; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + NSError *error = nil; + videoRegex = [NSRegularExpression regularExpressionWithPattern:@"(?:&|^)ext=MOV(?:&|$)" + options:NSRegularExpressionCaseInsensitive + error:&error]; + if (error) { + RCTLogError(@"%@", error); } + }); + + NSString *query = requestURL.query; + if ( + query != nil && + [videoRegex firstMatchInString:query + options:0 + range:NSMakeRange(0, query.length)] + ) { return NO; + } + + for (id loader in _loaders) { + // Don't use RCTImageURLLoader protocol for modules that already conform to + // RCTURLRequestHandler as it's inefficient to decode an image and then + // convert it back into data + if (![loader conformsToProtocol:@protocol(RCTURLRequestHandler)] && + [loader canLoadImageURL:requestURL]) { + return YES; + } + } + + return NO; } - (id)sendRequest:(NSURLRequest *)request withDelegate:(id)delegate diff --git a/Libraries/Image/RCTImageShadowView.h b/Libraries/Image/RCTImageShadowView.h index 168f0f4c1de86d..f79281e731ad99 100644 --- a/Libraries/Image/RCTImageShadowView.h +++ b/Libraries/Image/RCTImageShadowView.h @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/Image/RCTImageShadowView.m b/Libraries/Image/RCTImageShadowView.m index c9ba82e32188cc..9102dc1b3b0b73 100644 --- a/Libraries/Image/RCTImageShadowView.m +++ b/Libraries/Image/RCTImageShadowView.m @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/Image/RCTImageStoreManager.h b/Libraries/Image/RCTImageStoreManager.h index 3f070ada1d27be..b1969044524ec5 100644 --- a/Libraries/Image/RCTImageStoreManager.h +++ b/Libraries/Image/RCTImageStoreManager.h @@ -1,4 +1,4 @@ -// Copyright (c) 2004-present, Facebook, Inc. +// 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. diff --git a/Libraries/Image/RCTImageStoreManager.m b/Libraries/Image/RCTImageStoreManager.m index 443885becfff2e..03656e4ea3312d 100644 --- a/Libraries/Image/RCTImageStoreManager.m +++ b/Libraries/Image/RCTImageStoreManager.m @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/Image/RCTImageUtils.h b/Libraries/Image/RCTImageUtils.h index d6090ac67ffb48..a4b8216c401dc2 100644 --- a/Libraries/Image/RCTImageUtils.h +++ b/Libraries/Image/RCTImageUtils.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2013-present, Facebook, Inc. + * 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. diff --git a/Libraries/Image/RCTImageUtils.m b/Libraries/Image/RCTImageUtils.m index 52fe3893307307..5470c8f782895a 100644 --- a/Libraries/Image/RCTImageUtils.m +++ b/Libraries/Image/RCTImageUtils.m @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/Image/RCTImageView.h b/Libraries/Image/RCTImageView.h index 70357c359b142e..38f82db455d12b 100644 --- a/Libraries/Image/RCTImageView.h +++ b/Libraries/Image/RCTImageView.h @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/Image/RCTImageView.m b/Libraries/Image/RCTImageView.m index a754bb1078dc3b..3392a54c56ed47 100644 --- a/Libraries/Image/RCTImageView.m +++ b/Libraries/Image/RCTImageView.m @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/Image/RCTImageViewManager.h b/Libraries/Image/RCTImageViewManager.h index f718a70246ef41..9aef96cd72a957 100644 --- a/Libraries/Image/RCTImageViewManager.h +++ b/Libraries/Image/RCTImageViewManager.h @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/Image/RCTImageViewManager.m b/Libraries/Image/RCTImageViewManager.m index bd382e4bf191d0..70c5cb98cd52ad 100644 --- a/Libraries/Image/RCTImageViewManager.m +++ b/Libraries/Image/RCTImageViewManager.m @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -82,4 +82,11 @@ - (UIView *)view }]; } +RCT_EXPORT_METHOD(queryCache:(NSArray *)requests + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) +{ + resolve([self.bridge.imageLoader getImageCacheStatus:requests]); +} + @end diff --git a/Libraries/Image/RCTLocalAssetImageLoader.h b/Libraries/Image/RCTLocalAssetImageLoader.h index 4cd79bd403c5a2..df3918ad548c1a 100644 --- a/Libraries/Image/RCTLocalAssetImageLoader.h +++ b/Libraries/Image/RCTLocalAssetImageLoader.h @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/Image/RCTLocalAssetImageLoader.m b/Libraries/Image/RCTLocalAssetImageLoader.m index 091cd70e0eafef..68407f38064ab0 100644 --- a/Libraries/Image/RCTLocalAssetImageLoader.m +++ b/Libraries/Image/RCTLocalAssetImageLoader.m @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/Image/RCTResizeMode.h b/Libraries/Image/RCTResizeMode.h index ce0fba5e31afad..9d49f2d32cfeb9 100644 --- a/Libraries/Image/RCTResizeMode.h +++ b/Libraries/Image/RCTResizeMode.h @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/Image/RCTResizeMode.m b/Libraries/Image/RCTResizeMode.m index 1094b7ff96e8c9..34027f8e8a2ec0 100644 --- a/Libraries/Image/RCTResizeMode.m +++ b/Libraries/Image/RCTResizeMode.m @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/Image/RelativeImageStub.js b/Libraries/Image/RelativeImageStub.js index dcdeb9b916ac45..f5592d18167102 100644 --- a/Libraries/Image/RelativeImageStub.js +++ b/Libraries/Image/RelativeImageStub.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/Image/TextInlineImageNativeComponent.js b/Libraries/Image/TextInlineImageNativeComponent.js new file mode 100644 index 00000000000000..6cbe56362576f9 --- /dev/null +++ b/Libraries/Image/TextInlineImageNativeComponent.js @@ -0,0 +1,17 @@ +/** + * 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 requireNativeComponent = require('requireNativeComponent'); + +const TextInlineImage = requireNativeComponent('RCTTextInlineImage'); + +module.exports = TextInlineImage; diff --git a/Libraries/Image/__tests__/__snapshots__/assetRelativePathInSnapshot.js.snap b/Libraries/Image/__tests__/__snapshots__/assetRelativePathInSnapshot-test.js.snap similarity index 100% rename from Libraries/Image/__tests__/__snapshots__/assetRelativePathInSnapshot.js.snap rename to Libraries/Image/__tests__/__snapshots__/assetRelativePathInSnapshot-test.js.snap diff --git a/Libraries/Image/__tests__/assetRelativePathInSnapshot-test.js b/Libraries/Image/__tests__/assetRelativePathInSnapshot-test.js new file mode 100644 index 00000000000000..13cb6ac075f81e --- /dev/null +++ b/Libraries/Image/__tests__/assetRelativePathInSnapshot-test.js @@ -0,0 +1,29 @@ +/** + * 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'; + +jest.disableAutomock(); + +const React = require('React'); +const ReactTestRenderer = require('react-test-renderer'); +const Image = require('Image'); +const View = require('View'); + +it('renders assets based on relative path', () => { + expect( + ReactTestRenderer.create( + + + + , + ), + ).toMatchSnapshot(); +}); diff --git a/Libraries/Image/__tests__/assetRelativePathInSnapshot.js b/Libraries/Image/__tests__/assetRelativePathInSnapshot.js deleted file mode 100644 index 973d7171c0cd56..00000000000000 --- a/Libraries/Image/__tests__/assetRelativePathInSnapshot.js +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Copyright (c) 2017-present, Facebook, Inc. - * - * 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'; - -jest.disableAutomock(); - -const React = require('React'); -const ReactTestRenderer = require('react-test-renderer'); -const Image = require('Image'); -const View = require('View'); - -it('renders assets based on relative path', () => { - expect( - ReactTestRenderer.create( - - - - , - ), - ).toMatchSnapshot(); -}); diff --git a/Libraries/Image/__tests__/resolveAssetSource-test.js b/Libraries/Image/__tests__/resolveAssetSource-test.js index c22f32a6520413..fb297899f6655a 100644 --- a/Libraries/Image/__tests__/resolveAssetSource-test.js +++ b/Libraries/Image/__tests__/resolveAssetSource-test.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/Image/assetPathUtils.js b/Libraries/Image/assetPathUtils.js new file mode 100644 index 00000000000000..402680cef72779 --- /dev/null +++ b/Libraries/Image/assetPathUtils.js @@ -0,0 +1,88 @@ +/** + * 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 + */ + +'use strict'; + +import type {PackagerAsset} from './AssetRegistry'; + +const androidScaleSuffix = { + '0.75': 'ldpi', + '1': 'mdpi', + '1.5': 'hdpi', + '2': 'xhdpi', + '3': 'xxhdpi', + '4': 'xxxhdpi', +}; + +/** + * FIXME: using number to represent discrete scale numbers is fragile in essence because of + * floating point numbers imprecision. + */ +function getAndroidAssetSuffix(scale: number): string { + if (scale.toString() in androidScaleSuffix) { + return androidScaleSuffix[scale.toString()]; + } + + throw new Error('no such scale ' + scale.toString()); +} + +// See https://developer.android.com/guide/topics/resources/drawable-resource.html +const drawableFileTypes = new Set([ + 'gif', + 'jpeg', + 'jpg', + 'png', + 'svg', + 'webp', + 'xml', +]); + +function getAndroidResourceFolderName(asset: PackagerAsset, scale: number) { + if (!drawableFileTypes.has(asset.type)) { + return 'raw'; + } + var suffix = getAndroidAssetSuffix(scale); + if (!suffix) { + throw new Error( + "Don't know which android drawable suffix to use for scale: " + + scale + + '\nAsset: ' + + JSON.stringify(asset, null, '\t') + + '\nPossible scales are:' + + JSON.stringify(androidScaleSuffix, null, '\t'), + ); + } + const androidFolder = 'drawable-' + suffix; + return androidFolder; +} + +function getAndroidResourceIdentifier(asset: PackagerAsset) { + var folderPath = getBasePath(asset); + return (folderPath + '/' + asset.name) + .toLowerCase() + .replace(/\//g, '_') // Encode folder structure in file name + .replace(/([^a-z0-9_])/g, '') // Remove illegal chars + .replace(/^assets_/, ''); // Remove "assets_" prefix +} + +function getBasePath(asset: PackagerAsset) { + var basePath = asset.httpServerLocation; + if (basePath[0] === '/') { + basePath = basePath.substr(1); + } + return basePath; +} + +module.exports = { + getAndroidAssetSuffix: getAndroidAssetSuffix, + getAndroidResourceFolderName: getAndroidResourceFolderName, + getAndroidResourceIdentifier: getAndroidResourceIdentifier, + getBasePath: getBasePath, +}; diff --git a/Libraries/Image/nativeImageSource.js b/Libraries/Image/nativeImageSource.js index 1fdbe1caa97615..ead514fab1af4d 100644 --- a/Libraries/Image/nativeImageSource.js +++ b/Libraries/Image/nativeImageSource.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -23,6 +23,7 @@ export type NativeImageSource = {| type NativeImageSourceSpec = {| +android?: string, +ios?: string, + +default?: string, // For more details on width and height, see // http://facebook.github.io/react-native/docs/images.html#why-not-automatically-size-everything @@ -47,7 +48,11 @@ type NativeImageSourceSpec = {| * */ function nativeImageSource(spec: NativeImageSourceSpec): Object { - let uri = Platform.select(spec); + let uri = Platform.select({ + android: spec.android, + default: spec.default, + ios: spec.ios, + }); if (uri == null) { console.warn( 'nativeImageSource(...): No image name supplied for `%s`:\n%s', diff --git a/Libraries/Image/resolveAssetSource.js b/Libraries/Image/resolveAssetSource.js index 9c811e574400b1..ebd19e3354fe6e 100644 --- a/Libraries/Image/resolveAssetSource.js +++ b/Libraries/Image/resolveAssetSource.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. diff --git a/Libraries/Inspector/BorderBox.js b/Libraries/Inspector/BorderBox.js index b172871df8e2f2..93d0dd158e14d1 100644 --- a/Libraries/Inspector/BorderBox.js +++ b/Libraries/Inspector/BorderBox.js @@ -1,11 +1,11 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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 + * @flow strict-local */ 'use strict'; diff --git a/Libraries/Inspector/BoxInspector.js b/Libraries/Inspector/BoxInspector.js index af373460053609..6097402a91c0c2 100644 --- a/Libraries/Inspector/BoxInspector.js +++ b/Libraries/Inspector/BoxInspector.js @@ -1,11 +1,11 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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 + * @flow strict-local */ 'use strict'; @@ -87,12 +87,6 @@ const styles = StyleSheet.create({ textAlign: 'left', top: -3, }, - buffer: { - fontSize: 10, - color: 'yellow', - flex: 1, - textAlign: 'center', - }, innerText: { color: 'yellow', fontSize: 12, diff --git a/Libraries/Inspector/ElementBox.js b/Libraries/Inspector/ElementBox.js index dea05dad737894..d360485bcbe5f5 100644 --- a/Libraries/Inspector/ElementBox.js +++ b/Libraries/Inspector/ElementBox.js @@ -1,11 +1,11 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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 + * @flow strict-local */ 'use strict'; diff --git a/Libraries/Inspector/ElementProperties.js b/Libraries/Inspector/ElementProperties.js index 1e360944716fd2..9b6c222ed5babd 100644 --- a/Libraries/Inspector/ElementProperties.js +++ b/Libraries/Inspector/ElementProperties.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -11,7 +11,6 @@ 'use strict'; const BoxInspector = require('BoxInspector'); -const PropTypes = require('prop-types'); const React = require('React'); const StyleInspector = require('StyleInspector'); const StyleSheet = require('StyleSheet'); @@ -24,32 +23,23 @@ const flattenStyle = require('flattenStyle'); const mapWithSeparator = require('mapWithSeparator'); const openFileInEditor = require('openFileInEditor'); -import type {DangerouslyImpreciseStyleProp} from 'StyleSheet'; +import type {ViewStyleProp} from 'StyleSheet'; -class ElementProperties extends React.Component<{ - hierarchy: Array<$FlowFixMe>, - style?: DangerouslyImpreciseStyleProp, - source?: { +type Props = $ReadOnly<{| + hierarchy: Array<{|name: string|}>, + style?: ?ViewStyleProp, + source?: ?{ fileName?: string, lineNumber?: number, }, -}> { - static propTypes = { - hierarchy: PropTypes.array.isRequired, - style: PropTypes.oneOfType([ - PropTypes.object, - PropTypes.array, - PropTypes.number, - ]), - source: PropTypes.shape({ - fileName: PropTypes.string, - lineNumber: PropTypes.number, - }), - }; + frame?: ?Object, + selection?: ?number, + setSelection?: number => mixed, +|}>; +class ElementProperties extends React.Component { render() { const style = flattenStyle(this.props.style); - // $FlowFixMe found when converting React.createClass to ES6 const selection = this.props.selection; let openFileButton; const source = this.props.source; @@ -96,10 +86,7 @@ class ElementProperties extends React.Component<{ {openFileButton} - { - // $FlowFixMe found when converting React.createClass to ES6 - - } + {} diff --git a/Libraries/Inspector/Inspector.js b/Libraries/Inspector/Inspector.js index a5ab122b0cde5b..56d23c16b1a3d8 100644 --- a/Libraries/Inspector/Inspector.js +++ b/Libraries/Inspector/Inspector.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -21,11 +21,7 @@ const Touchable = require('Touchable'); const UIManager = require('UIManager'); const View = require('View'); -/* $FlowFixMe(>=0.54.0 site=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. */ -const emptyObject = require('fbjs/lib/emptyObject'); -const invariant = require('fbjs/lib/invariant'); +const invariant = require('invariant'); export type ReactRenderer = { getInspectorDataForViewTag: (viewTag: number) => Object, @@ -51,14 +47,20 @@ function findRenderers(): $ReadOnlyArray { function getInspectorDataForViewTag(touchedViewTag: number) { for (let i = 0; i < renderers.length; i++) { const renderer = renderers[i]; - const inspectorData = renderer.getInspectorDataForViewTag(touchedViewTag); - if (inspectorData.hierarchy.length > 0) { - return inspectorData; + if ( + Object.prototype.hasOwnProperty.call( + renderer, + 'getInspectorDataForViewTag', + ) + ) { + const inspectorData = renderer.getInspectorDataForViewTag(touchedViewTag); + if (inspectorData.hierarchy.length > 0) { + return inspectorData; + } } } throw new Error('Expected to find at least one React renderer.'); } - class Inspector extends React.Component< { inspectedViewTag: ?number, @@ -116,9 +118,6 @@ class Inspector extends React.Component< attachToDevtools = (agent: Object) => { let _hideWait = null; const hlSub = agent.sub('highlight', ({node, name, props}) => { - /* $FlowFixMe(>=0.63.0 site=react_native_fb) This comment suppresses an - * error found when Flow v0.63 was deployed. To see the error delete this - * comment and run Flow. */ clearTimeout(_hideWait); if (typeof node !== 'number') { @@ -131,7 +130,7 @@ class Inspector extends React.Component< hierarchy: [], inspected: { frame: {left, top, width, height}, - style: props ? props.style : emptyObject, + style: props ? props.style : {}, }, }); }); diff --git a/Libraries/Inspector/InspectorOverlay.js b/Libraries/Inspector/InspectorOverlay.js index 4de9cb2ab8183e..88bf38c3b27431 100644 --- a/Libraries/Inspector/InspectorOverlay.js +++ b/Libraries/Inspector/InspectorOverlay.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -12,34 +12,27 @@ const Dimensions = require('Dimensions'); const ElementBox = require('ElementBox'); -const PropTypes = require('prop-types'); const React = require('React'); const StyleSheet = require('StyleSheet'); const UIManager = require('UIManager'); const View = require('View'); -type EventLike = { - nativeEvent: Object, -}; +import type {PressEvent} from 'CoreEventTypes'; +import type {ViewStyleProp} from 'StyleSheet'; -class InspectorOverlay extends React.Component<{ - inspected?: { - frame?: Object, - style?: any, - }, - inspectedViewTag?: number, - onTouchViewTag: (tag: number, frame: Object, pointerY: number) => void, -}> { - static propTypes = { - inspected: PropTypes.shape({ - frame: PropTypes.object, - style: PropTypes.any, - }), - inspectedViewTag: PropTypes.number, - onTouchViewTag: PropTypes.func.isRequired, - }; +type Inspected = $ReadOnly<{| + frame?: Object, + style?: ViewStyleProp, +|}>; + +type Props = $ReadOnly<{| + inspected?: Inspected, + inspectedViewTag?: ?number, + onTouchViewTag: (tag: number, frame: Object, pointerY: number) => mixed, +|}>; - findViewForTouchEvent = (e: EventLike) => { +class InspectorOverlay extends React.Component { + findViewForTouchEvent = (e: PressEvent) => { const {locationX, locationY} = e.nativeEvent.touches[0]; UIManager.findSubviewIn( this.props.inspectedViewTag, @@ -54,7 +47,7 @@ class InspectorOverlay extends React.Component<{ ); }; - shouldSetResponser = (e: EventLike): boolean => { + shouldSetResponser = (e: PressEvent): boolean => { this.findViewForTouchEvent(e); return true; }; diff --git a/Libraries/Inspector/InspectorPanel.js b/Libraries/Inspector/InspectorPanel.js index 2793452d90e578..42c191d7ed530c 100644 --- a/Libraries/Inspector/InspectorPanel.js +++ b/Libraries/Inspector/InspectorPanel.js @@ -1,11 +1,11 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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 + * @flow strict-local */ 'use strict'; @@ -14,14 +14,43 @@ const ElementProperties = require('ElementProperties'); const NetworkOverlay = require('NetworkOverlay'); const PerformanceOverlay = require('PerformanceOverlay'); const React = require('React'); -const PropTypes = require('prop-types'); const ScrollView = require('ScrollView'); const StyleSheet = require('StyleSheet'); const Text = require('Text'); const TouchableHighlight = require('TouchableHighlight'); const View = require('View'); -class InspectorPanel extends React.Component<$FlowFixMeProps> { +import type {ViewStyleProp} from 'StyleSheet'; + +type Props = $ReadOnly<{| + devtoolsIsOpen: boolean, + inspecting: boolean, + setInspecting: (val: boolean) => void, + perfing: boolean, + setPerfing: (val: boolean) => void, + touchTargeting: boolean, + setTouchTargeting: (val: boolean) => void, + networking: boolean, + setNetworking: (val: boolean) => void, + hierarchy?: ?Array<{|name: string|}>, + selection?: ?number, + setSelection: number => mixed, + inspected?: ?$ReadOnly<{| + style?: ?ViewStyleProp, + frame?: ?$ReadOnly<{| + top?: ?number, + left?: ?number, + width?: ?number, + height: ?number, + |}>, + source?: ?{| + fileName?: string, + lineNumber?: number, + |}, + |}>, +|}>; + +class InspectorPanel extends React.Component { renderWaiting() { if (this.props.inspecting) { return ( @@ -40,6 +69,7 @@ class InspectorPanel extends React.Component<$FlowFixMeProps> { style={this.props.inspected.style} frame={this.props.inspected.frame} source={this.props.inspected.source} + // $FlowFixMe: Hierarchy should be non-nullable hierarchy={this.props.hierarchy} selection={this.props.selection} setSelection={this.props.setSelection} @@ -57,22 +87,22 @@ class InspectorPanel extends React.Component<$FlowFixMeProps> { {!this.props.devtoolsIsOpen && contents} -