diff --git a/.eslintrc.js b/.eslintrc.js index 2e166ca41..86c9c995f 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -10,7 +10,8 @@ module.exports = { extends: [ "airbnb", "plugin:i18next/recommended", - "plugin:@tanstack/eslint-plugin-query/recommended" + "plugin:@tanstack/eslint-plugin-query/recommended", + "plugin:react-native-a11y/ios" ], plugins: [ "module-resolver", @@ -28,12 +29,15 @@ module.exports = { "consistent-return": [2, { treatUndefinedAsUnspecified: true }], "func-names": 0, "global-require": 0, - "i18next/no-literal-string": [2, { - words: { - // Minor change to the default to disallow all-caps string literals as well - exclude: ["[0-9!-/:-@[-`{-~]+"] + "i18next/no-literal-string": [ + 2, + { + words: { + // Minor change to the default to disallow all-caps string literals as well + exclude: ["[0-9!-/:-@[-`{-~]+"] + } } - }], + ], // The AirBNB approach at // https://github.com/airbnb/javascript/blob/master/packages/eslint-config-airbnb-base/rules/imports.js#L71 // is quite particular and forbids imports of devDependencies anywhere @@ -45,22 +49,30 @@ module.exports = { // raise alarms when you try to import things not declared in // package.json. "import/no-extraneous-dependencies": ["error", {}], - "max-len": ["error", 100, 2, { - ignoreUrls: true, - ignoreComments: false, - ignoreRegExpLiterals: true, - ignoreStrings: false, - ignoreTemplateLiterals: false - }], + "max-len": [ + "error", + 100, + 2, + { + ignoreUrls: true, + ignoreComments: false, + ignoreRegExpLiterals: true, + ignoreStrings: false, + ignoreTemplateLiterals: false + } + ], "no-alert": 0, "no-underscore-dangle": 0, - "no-unused-vars": ["error", { - vars: "all", - args: "after-used", - // Overriding airbnb to allow leading underscore to indicate unused var - argsIgnorePattern: "^_", - ignoreRestSiblings: true - }], + "no-unused-vars": [ + "error", + { + vars: "all", + args: "after-used", + // Overriding airbnb to allow leading underscore to indicate unused var + argsIgnorePattern: "^_", + ignoreRestSiblings: true + } + ], "no-void": 0, "prefer-destructuring": [2, { object: true, array: false }], quotes: [2, "double"], @@ -76,7 +88,10 @@ module.exports = { "react/prop-types": 0, "react/destructuring-assignment": 0, "react/jsx-filename-extension": 0, - "react/function-component-definition": [2, { namedComponents: "arrow-function" }], + "react/function-component-definition": [ + 2, + { namedComponents: "arrow-function" } + ], "react/require-default-props": 0, // React-Hooks Plugin @@ -87,7 +102,21 @@ module.exports = { "react-native/no-inline-styles": "error", "simple-import-sort/imports": "error", - "simple-import-sort/exports": "error" + "simple-import-sort/exports": "error", + "react-native-a11y/has-accessibility-hint": 1, + "react-native-a11y/has-accessibility-props": 1, + "react-native-a11y/has-valid-accessibility-actions": 1, + "react-native-a11y/has-valid-accessibility-role": 1, + "react-native-a11y/has-valid-accessibility-state": 1, + "react-native-a11y/has-valid-accessibility-states": 1, + "react-native-a11y/has-valid-accessibility-component-type": 1, + "react-native-a11y/has-valid-accessibility-traits": 1, + "react-native-a11y/has-valid-accessibility-value": 1, + "react-native-a11y/no-nested-touchables": 1, + "react-native-a11y/has-valid-accessibility-descriptors": 1, + "react-native-a11y/has-valid-accessibility-ignores-invert-colors": 1, + "react-native-a11y/has-valid-accessibility-live-region": 1, + "react-native-a11y/has-valid-important-for-accessibility": 1 }, // need this so jest doesn't show as undefined in jest.setup.js env: { diff --git a/.github/workflows/e2e_ios.yml b/.github/workflows/e2e_ios.yml index 7e0b216ac..9b93d2bdb 100644 --- a/.github/workflows/e2e_ios.yml +++ b/.github/workflows/e2e_ios.yml @@ -75,10 +75,10 @@ jobs: - run: brew install applesimutils - name: Build test app - run: npm run e2e:build + run: npm run e2e:build:ios - name: Run e2e test - run: npm run e2e:test -- --cleanup --debug-synchronization 200 --take-screenshots failing --record-videos failing -l trace + run: npm run e2e:test:ios -- --cleanup --debug-synchronization 200 --take-screenshots failing --record-videos failing -l trace # The artifacts for the failing tests are available for download on github.com on the page of the individual actions run - name: Store Detox artifacts on test failure diff --git a/README.md b/README.md index 7bf9837cb..06e7ef97a 100644 --- a/README.md +++ b/README.md @@ -127,7 +127,7 @@ We're using Nativewind, a styling system for React Native based on Tailwind CSS. 1. Download custom icon from Figma as an SVG file. 2. Add new icon to the iNaturalist icon set in Fontastic. Select all relevant iNaturalist icons, tap the Publish tab, and download the zip of icons. -3. Create a glpyh file from the CSS file you just downloaded, using the following command (be sure to replace /path/to/styles with your path): +3. Create a glyph file from the CSS file you just downloaded, using the following command (be sure to replace /path/to/styles with your path): ``` ./node_modules/.bin/generate-icon '/path/to/styles.css' --componentName=INatIcon --fontFamily=inaturalisticons > 'src/components/INatIcon.js' diff --git a/android/app/src/main/assets/fonts/Whitney-Book-Pro.otf b/android/app/src/main/assets/fonts/Whitney-Book-Pro.otf deleted file mode 100644 index f50bc45e2..000000000 Binary files a/android/app/src/main/assets/fonts/Whitney-Book-Pro.otf and /dev/null differ diff --git a/android/app/src/main/assets/fonts/Whitney-BookItalic-Pro.otf b/android/app/src/main/assets/fonts/Whitney-BookItalic-Pro.otf deleted file mode 100644 index 7b3fae7be..000000000 Binary files a/android/app/src/main/assets/fonts/Whitney-BookItalic-Pro.otf and /dev/null differ diff --git a/android/app/src/main/assets/fonts/Whitney-Semibold-Pro.otf b/android/app/src/main/assets/fonts/Whitney-Semibold-Pro.otf deleted file mode 100644 index d926fefc3..000000000 Binary files a/android/app/src/main/assets/fonts/Whitney-Semibold-Pro.otf and /dev/null differ diff --git a/android/app/src/main/assets/fonts/Whitney-SemiboldItalic-Pro.otf b/android/app/src/main/assets/fonts/Whitney-SemiboldItalic-Pro.otf deleted file mode 100644 index 5f6d06972..000000000 Binary files a/android/app/src/main/assets/fonts/Whitney-SemiboldItalic-Pro.otf and /dev/null differ diff --git a/android/link-assets-manifest.json b/android/link-assets-manifest.json index 50c1d4dd1..43f5b8f97 100644 --- a/android/link-assets-manifest.json +++ b/android/link-assets-manifest.json @@ -1,14 +1,6 @@ { "migIndex": 1, "data": [ - { - "path": "assets/fonts/Whitney-Book-Pro.otf", - "sha1": "639b8a8bf3e1cc3de30d0f49e666aca3999ca65a" - }, - { - "path": "assets/fonts/Whitney-BookItalic-Pro.otf", - "sha1": "15854f60175a0e82b794c259431ec45ea4b40103" - }, { "path": "assets/fonts/Whitney-Light-Pro.otf", "sha1": "d15560faea2b18aef9867a1d2b9e2efb54b17b5b" @@ -17,14 +9,6 @@ "path": "assets/fonts/Whitney-Medium-Pro.otf", "sha1": "33ca073c11f46dc266a7dc1adeaa102891bd76d1" }, - { - "path": "assets/fonts/Whitney-Semibold-Pro.otf", - "sha1": "7a107095a453c8cd8046d9ccded5d88e6809e89f" - }, - { - "path": "assets/fonts/Whitney-SemiboldItalic-Pro.otf", - "sha1": "53e8a56ceb630b63316db41017125c52fbf0e845" - }, { "path": "assets/fonts/inaturalisticons.ttf", "sha1": "76435d11edb09f7914ebf9cb388db461c50d7fda" diff --git a/assets/fonts/Whitney-Book-Pro.otf b/assets/fonts/Whitney-Book-Pro.otf deleted file mode 100644 index f50bc45e2..000000000 Binary files a/assets/fonts/Whitney-Book-Pro.otf and /dev/null differ diff --git a/assets/fonts/Whitney-BookItalic-Pro.otf b/assets/fonts/Whitney-BookItalic-Pro.otf deleted file mode 100644 index 7b3fae7be..000000000 Binary files a/assets/fonts/Whitney-BookItalic-Pro.otf and /dev/null differ diff --git a/assets/fonts/Whitney-Semibold-Pro.otf b/assets/fonts/Whitney-Semibold-Pro.otf deleted file mode 100644 index d926fefc3..000000000 Binary files a/assets/fonts/Whitney-Semibold-Pro.otf and /dev/null differ diff --git a/assets/fonts/Whitney-SemiboldItalic-Pro.otf b/assets/fonts/Whitney-SemiboldItalic-Pro.otf deleted file mode 100644 index 5f6d06972..000000000 Binary files a/assets/fonts/Whitney-SemiboldItalic-Pro.otf and /dev/null differ diff --git a/e2e/addObservationWithoutData.e2e.js b/e2e/addObservationWithoutData.e2e.js index 50b612a42..202c3581b 100644 --- a/e2e/addObservationWithoutData.e2e.js +++ b/e2e/addObservationWithoutData.e2e.js @@ -34,15 +34,15 @@ describe( "Add observation without evidence", () => { } ); it( "should navigate to observation add screen on add evidence button pressed", async () => { - await waitFor( element( by.id( "camera-options-button" ) ) ) + await waitFor( element( by.id( "add-obs-button" ) ) ) .toBeVisible() .withTimeout( 10000 ); - await element( by.id( "camera-options-button" ) ).tap(); + await element( by.id( "add-obs-button" ) ).tap(); await expect( element( by.id( "evidence-text" ) ) ).toBeVisible(); await expect( - element( by.id( "camera-options-button-square-edit-outline" ) ) + element( by.id( "observe-without-evidence-button" ) ) ).toBeVisible(); - await element( by.id( "camera-options-button-square-edit-outline" ) ).tap(); + await element( by.id( "observe-without-evidence-button" ) ).tap(); await waitFor( element( by.id( "new-observation-text" ) ) ).toBeVisible().withTimeout( 10000 ); } ); } ); diff --git a/ios/iNaturalistReactNative.xcodeproj/project.pbxproj b/ios/iNaturalistReactNative.xcodeproj/project.pbxproj index a0941cb69..e17da131c 100644 --- a/ios/iNaturalistReactNative.xcodeproj/project.pbxproj +++ b/ios/iNaturalistReactNative.xcodeproj/project.pbxproj @@ -7,19 +7,15 @@ objects = { /* Begin PBXBuildFile section */ + 0085E50460D749DB8D818C1F /* inaturalisticons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 81076C08FFF24C36A0725EEE /* inaturalisticons.ttf */; }; 00E356F31AD99517003FC87E /* iNaturalistReactNativeTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 00E356F21AD99517003FC87E /* iNaturalistReactNativeTests.m */; }; 13B07FBC1A68108700A75B9A /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.mm */; }; 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; 13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; }; 7EBBE0515B6D88FD724A5C47 /* libPods-iNaturalistReactNative.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7DD42FAECED8C66268DDB37F /* libPods-iNaturalistReactNative.a */; }; 81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; }; - 829672AB5DD3412FB120DE40 /* Whitney-BookItalic-Pro.otf in Resources */ = {isa = PBXBuildFile; fileRef = B5359D98B3384738A8ED6C74 /* Whitney-BookItalic-Pro.otf */; }; A252B2AEA64E47C9AC1D20E8 /* Whitney-Light-Pro.otf in Resources */ = {isa = PBXBuildFile; fileRef = BA9D41ECEBFA4C38B74009B3 /* Whitney-Light-Pro.otf */; }; - B5241D089ADB4290B5566898 /* Whitney-Semibold-Pro.otf in Resources */ = {isa = PBXBuildFile; fileRef = C6C5CABEB9CB44B5ABF940A8 /* Whitney-Semibold-Pro.otf */; }; BA2479FA3D7B40A7BEF7B3CD /* Whitney-Medium-Pro.otf in Resources */ = {isa = PBXBuildFile; fileRef = D09FA3A0162844FF80A5EF96 /* Whitney-Medium-Pro.otf */; }; - C8EEAD27B69C424BAB2EB23F /* Whitney-SemiboldItalic-Pro.otf in Resources */ = {isa = PBXBuildFile; fileRef = A788948E590442F385C5FD6C /* Whitney-SemiboldItalic-Pro.otf */; }; - CF25D941972D42BDBBD309E0 /* Whitney-Book-Pro.otf in Resources */ = {isa = PBXBuildFile; fileRef = 8860EC2616494880B36297C2 /* Whitney-Book-Pro.otf */; }; - 0085E50460D749DB8D818C1F /* inaturalisticons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 81076C08FFF24C36A0725EEE /* inaturalisticons.ttf */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -45,16 +41,12 @@ 19A5877328F8E3310016D128 /* iNaturalistReactNative-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "iNaturalistReactNative-Bridging-Header.h"; sourceTree = ""; }; 27F9BBBBAB49ABBAF88433C9 /* Pods-iNaturalistReactNative.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-iNaturalistReactNative.debug.xcconfig"; path = "Target Support Files/Pods-iNaturalistReactNative/Pods-iNaturalistReactNative.debug.xcconfig"; sourceTree = ""; }; 7DD42FAECED8C66268DDB37F /* libPods-iNaturalistReactNative.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-iNaturalistReactNative.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 81076C08FFF24C36A0725EEE /* inaturalisticons.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = undefined; includeInIndex = 0; lastKnownFileType = unknown; name = inaturalisticons.ttf; path = ../assets/fonts/inaturalisticons.ttf; sourceTree = ""; }; 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = iNaturalistReactNative/LaunchScreen.storyboard; sourceTree = ""; }; - 8860EC2616494880B36297C2 /* Whitney-Book-Pro.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "Whitney-Book-Pro.otf"; path = "../assets/fonts/Whitney-Book-Pro.otf"; sourceTree = ""; }; - A788948E590442F385C5FD6C /* Whitney-SemiboldItalic-Pro.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "Whitney-SemiboldItalic-Pro.otf"; path = "../assets/fonts/Whitney-SemiboldItalic-Pro.otf"; sourceTree = ""; }; - B5359D98B3384738A8ED6C74 /* Whitney-BookItalic-Pro.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "Whitney-BookItalic-Pro.otf"; path = "../assets/fonts/Whitney-BookItalic-Pro.otf"; sourceTree = ""; }; BA9D41ECEBFA4C38B74009B3 /* Whitney-Light-Pro.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "Whitney-Light-Pro.otf"; path = "../assets/fonts/Whitney-Light-Pro.otf"; sourceTree = ""; }; C544256CF572EB52B9E2B9CB /* Pods-iNaturalistReactNative.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-iNaturalistReactNative.release.xcconfig"; path = "Target Support Files/Pods-iNaturalistReactNative/Pods-iNaturalistReactNative.release.xcconfig"; sourceTree = ""; }; - C6C5CABEB9CB44B5ABF940A8 /* Whitney-Semibold-Pro.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "Whitney-Semibold-Pro.otf"; path = "../assets/fonts/Whitney-Semibold-Pro.otf"; sourceTree = ""; }; D09FA3A0162844FF80A5EF96 /* Whitney-Medium-Pro.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "Whitney-Medium-Pro.otf"; path = "../assets/fonts/Whitney-Medium-Pro.otf"; sourceTree = ""; }; ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; - 81076C08FFF24C36A0725EEE /* inaturalisticons.ttf */ = {isa = PBXFileReference; name = "inaturalisticons.ttf"; path = "../assets/fonts/inaturalisticons.ttf"; sourceTree = ""; fileEncoding = undefined; lastKnownFileType = unknown; explicitFileType = undefined; includeInIndex = 0; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -160,12 +152,8 @@ E7A0E0AA690545D78AA7CC73 /* Resources */ = { isa = PBXGroup; children = ( - 8860EC2616494880B36297C2 /* Whitney-Book-Pro.otf */, - B5359D98B3384738A8ED6C74 /* Whitney-BookItalic-Pro.otf */, BA9D41ECEBFA4C38B74009B3 /* Whitney-Light-Pro.otf */, D09FA3A0162844FF80A5EF96 /* Whitney-Medium-Pro.otf */, - C6C5CABEB9CB44B5ABF940A8 /* Whitney-Semibold-Pro.otf */, - A788948E590442F385C5FD6C /* Whitney-SemiboldItalic-Pro.otf */, 81076C08FFF24C36A0725EEE /* inaturalisticons.ttf */, ); name = Resources; @@ -264,12 +252,8 @@ files = ( 81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */, 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */, - CF25D941972D42BDBBD309E0 /* Whitney-Book-Pro.otf in Resources */, - 829672AB5DD3412FB120DE40 /* Whitney-BookItalic-Pro.otf in Resources */, A252B2AEA64E47C9AC1D20E8 /* Whitney-Light-Pro.otf in Resources */, BA2479FA3D7B40A7BEF7B3CD /* Whitney-Medium-Pro.otf in Resources */, - B5241D089ADB4290B5566898 /* Whitney-Semibold-Pro.otf in Resources */, - C8EEAD27B69C424BAB2EB23F /* Whitney-SemiboldItalic-Pro.otf in Resources */, 0085E50460D749DB8D818C1F /* inaturalisticons.ttf in Resources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/ios/iNaturalistReactNative/Info.plist b/ios/iNaturalistReactNative/Info.plist index b2c36e17c..ebc01fa8f 100644 --- a/ios/iNaturalistReactNative/Info.plist +++ b/ios/iNaturalistReactNative/Info.plist @@ -55,12 +55,8 @@ MaterialCommunityIcons.ttf MaterialIcons.ttf - Whitney-Book-Pro.otf - Whitney-BookItalic-Pro.otf Whitney-Light-Pro.otf Whitney-Medium-Pro.otf - Whitney-Semibold-Pro.otf - Whitney-SemiboldItalic-Pro.otf inaturalisticons.ttf UILaunchStoryboardName diff --git a/ios/link-assets-manifest.json b/ios/link-assets-manifest.json index 50c1d4dd1..43f5b8f97 100644 --- a/ios/link-assets-manifest.json +++ b/ios/link-assets-manifest.json @@ -1,14 +1,6 @@ { "migIndex": 1, "data": [ - { - "path": "assets/fonts/Whitney-Book-Pro.otf", - "sha1": "639b8a8bf3e1cc3de30d0f49e666aca3999ca65a" - }, - { - "path": "assets/fonts/Whitney-BookItalic-Pro.otf", - "sha1": "15854f60175a0e82b794c259431ec45ea4b40103" - }, { "path": "assets/fonts/Whitney-Light-Pro.otf", "sha1": "d15560faea2b18aef9867a1d2b9e2efb54b17b5b" @@ -17,14 +9,6 @@ "path": "assets/fonts/Whitney-Medium-Pro.otf", "sha1": "33ca073c11f46dc266a7dc1adeaa102891bd76d1" }, - { - "path": "assets/fonts/Whitney-Semibold-Pro.otf", - "sha1": "7a107095a453c8cd8046d9ccded5d88e6809e89f" - }, - { - "path": "assets/fonts/Whitney-SemiboldItalic-Pro.otf", - "sha1": "53e8a56ceb630b63316db41017125c52fbf0e845" - }, { "path": "assets/fonts/inaturalisticons.ttf", "sha1": "76435d11edb09f7914ebf9cb388db461c50d7fda" diff --git a/package-lock.json b/package-lock.json index 7d16ebb68..91ca57018 100644 --- a/package-lock.json +++ b/package-lock.json @@ -113,7 +113,9 @@ "eslint-plugin-react": "^7.29.4", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-native": "^4.0.0", + "eslint-plugin-react-native-a11y": "^3.3.0", "eslint-plugin-simple-import-sort": "^7.0.0", + "eslint-plugin-testing-library": "^5.10.0", "factoria": "^3.2.2", "faker": "^5.5.3", "flow-bin": "^0.182.0", @@ -9709,6 +9711,23 @@ "eslint": "^3.17.0 || ^4 || ^5 || ^6 || ^7 || ^8" } }, + "node_modules/eslint-plugin-react-native-a11y": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-native-a11y/-/eslint-plugin-react-native-a11y-3.3.0.tgz", + "integrity": "sha512-21bIs/0yROcMq7KtAG+OVNDWAh8M+6scII0iXcO3i9NYHe2xZ443yPs5KSUMSvQJeRLLjuKB7V5saqNjoMWDHA==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.15.4", + "ast-types-flow": "^0.0.7", + "jsx-ast-utils": "^3.2.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" + } + }, "node_modules/eslint-plugin-react-native-globals": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/eslint-plugin-react-native-globals/-/eslint-plugin-react-native-globals-0.1.2.tgz", @@ -9753,6 +9772,22 @@ "eslint": ">=5.0.0" } }, + "node_modules/eslint-plugin-testing-library": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-testing-library/-/eslint-plugin-testing-library-5.10.0.tgz", + "integrity": "sha512-aTOsCAEI9trrX3TLOnsskfhe57DmsjP/yMKLPqg4ftdRvfR4qut2PGWUa8TwP7whZbwMzJjh98tgAPcE8vdHow==", + "dev": true, + "dependencies": { + "@typescript-eslint/utils": "^5.43.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0", + "npm": ">=6" + }, + "peerDependencies": { + "eslint": "^7.5.0 || ^8.0.0" + } + }, "node_modules/eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", @@ -30549,6 +30584,17 @@ "eslint-plugin-react-native-globals": "^0.1.1" } }, + "eslint-plugin-react-native-a11y": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-native-a11y/-/eslint-plugin-react-native-a11y-3.3.0.tgz", + "integrity": "sha512-21bIs/0yROcMq7KtAG+OVNDWAh8M+6scII0iXcO3i9NYHe2xZ443yPs5KSUMSvQJeRLLjuKB7V5saqNjoMWDHA==", + "dev": true, + "requires": { + "@babel/runtime": "^7.15.4", + "ast-types-flow": "^0.0.7", + "jsx-ast-utils": "^3.2.1" + } + }, "eslint-plugin-react-native-globals": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/eslint-plugin-react-native-globals/-/eslint-plugin-react-native-globals-0.1.2.tgz", @@ -30562,6 +30608,15 @@ "dev": true, "requires": {} }, + "eslint-plugin-testing-library": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-testing-library/-/eslint-plugin-testing-library-5.10.0.tgz", + "integrity": "sha512-aTOsCAEI9trrX3TLOnsskfhe57DmsjP/yMKLPqg4ftdRvfR4qut2PGWUa8TwP7whZbwMzJjh98tgAPcE8vdHow==", + "dev": true, + "requires": { + "@typescript-eslint/utils": "^5.43.0" + } + }, "eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", diff --git a/package.json b/package.json index 207730adf..b2c9e82b3 100644 --- a/package.json +++ b/package.json @@ -9,15 +9,17 @@ "clean-start": "npx react-native clean-project-auto && npx pod-install && npm start", "test": "jest", "lint": "npm run lint:eslint && npm run lint:flow", - "lint:eslint": "eslint . --fix", + "lint:eslint": "eslint . --fix --quiet", "lint:flow": "flow check", "postinstall": "husky install", "translate": "node src/i18n/i18ncli.js build", "e2e:build:android": "npx detox build --configuration android.release", - "e2e:build": "npx detox build --configuration ios.release", + "e2e:build:ios": "npx detox build --configuration ios.release", + "e2e:build": "npm run e2e:build:ios && npm run e2e:build:android", "e2e:test:android": "npx detox test --configuration android.release", - "e2e:test": "npx detox test --configuration ios.release", - "e2e": "npm run e2e:build && npm run e2e:test" + "e2e:test:ios": "npx detox test --configuration ios.release", + "e2e:test": "npm run e2e:test:ios && npm run e2e:test:android", + "e2e": "npm run e2e:build && npm run e2e:test " }, "dependencies": { "@babel/eslint-parser": "^7.18.2", @@ -124,7 +126,9 @@ "eslint-plugin-react": "^7.29.4", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-native": "^4.0.0", + "eslint-plugin-react-native-a11y": "^3.3.0", "eslint-plugin-simple-import-sort": "^7.0.0", + "eslint-plugin-testing-library": "^5.10.0", "factoria": "^3.2.2", "faker": "^5.5.3", "flow-bin": "^0.182.0", diff --git a/src/components/AddObsModal.js b/src/components/AddObsModal.js index 9b7f78c07..085170ba1 100644 --- a/src/components/AddObsModal.js +++ b/src/components/AddObsModal.js @@ -46,10 +46,10 @@ const AddObsModal = ( { closeModal }: Props ): React.Node => { t( "Record-a-sound" ) ]; - const renderIconButton = ( icon, className, onPress, accessibilityLabel, size = 30 ) => ( + const renderIconButton = ( icon, className, onPress, accessibilityLabel, testID ) => ( { - {renderIconButton( "camera", "mx-5", navToStandardCamera, t( "Navigate-to-camera" ) )} + {renderIconButton( + "camera", + "mx-5", + navToStandardCamera, + t( "Navigate-to-camera" ), + "camera-button" + )} {renderIconButton( "icon-gallery", "mx-5", navToPhotoGallery, - t( "Navigate-to-photo-importer" ) + t( "Navigate-to-photo-importer" ), + "import-media-button" )} @@ -87,19 +94,22 @@ const AddObsModal = ( { closeModal }: Props ): React.Node => { "pen-and-paper", "mx-2", navToObsEdit, - t( "Navigate-to-observation-edit-screen" ) + t( "Navigate-to-observation-edit-screen" ), + "observe-without-evidence-button" )} {renderIconButton( "close-button-x", "self-center h-24 w-24 rounded-[99px]", ( ) => closeModal( ), - t( "Close-camera-options-modal" ) + t( "Close-camera-options-modal" ), + "close-camera-options-button" )} {renderIconButton( "microphone", "mx-2", navToSoundRecorder, - t( "Navigate-to-sound-recorder" ) + t( "Navigate-to-sound-recorder" ), + "record-sound-button" )} diff --git a/src/components/Observations/LoggedOutCard.js b/src/components/Observations/LoggedOutCard.js index 58dbea911..c4294266f 100644 --- a/src/components/Observations/LoggedOutCard.js +++ b/src/components/Observations/LoggedOutCard.js @@ -17,15 +17,15 @@ const LoggedOutCard = ( ): Node => { onPress={( ) => navigation.navigate( "login" )} accessibilityRole="link" accessibilityLabel={t( "Navigate-to-login-screen" )} - className="rounded-bl-3xl rounded-br-3xl bg-primary h-24 justify-center" + className="rounded-bl-3xl rounded-br-3xl h-24 justify-center" > {t( "Log-in-to-iNaturalist" )} - + {t( "X-unuploaded-observations", { observationCount: numUnuploadedObs } )} diff --git a/src/components/Observations/ObsListBottomSheet.js b/src/components/Observations/ObsListBottomSheet.js index d2541993c..da56fc665 100644 --- a/src/components/Observations/ObsListBottomSheet.js +++ b/src/components/Observations/ObsListBottomSheet.js @@ -4,13 +4,8 @@ import BottomSheet from "components/SharedComponents/BottomSheet"; import type { Node } from "react"; import React from "react"; import useCurrentUser from "sharedHooks/useCurrentUser"; -import useLocalObservations from "sharedHooks/useLocalObservations"; -import useNumUnuploadedObservations from "sharedHooks/useNumUnuploadedObservations"; -import useUploadObservations from "sharedHooks/useUploadObservations"; import LoginPrompt from "./LoginPrompt"; -import UploadProgressBar from "./UploadProgressBar"; -import UploadPrompt from "./UploadPrompt"; type Props = { hasScrolled: boolean @@ -18,14 +13,6 @@ type Props = { const ObsListBottomSheet = ( { hasScrolled }: Props ): Node => { const currentUser = useCurrentUser( ); - const { allObsToUpload } = useLocalObservations( ); - const numUnuploadedObs = useNumUnuploadedObservations( ); - - const { - stopUpload, - uploadInProgress, - startUpload - } = useUploadObservations( allObsToUpload ); if ( !currentUser ) { return ( @@ -35,21 +22,6 @@ const ObsListBottomSheet = ( { hasScrolled }: Props ): Node => { ); } - if ( uploadInProgress ) { - return ( - - ); - } - if ( numUnuploadedObs > 0 && currentUser ) { - return ( - - - - ); - } return null; }; diff --git a/src/components/Observations/ObsListHeader.js b/src/components/Observations/ObsListHeader.js index 75e9dab80..0abadbc58 100644 --- a/src/components/Observations/ObsListHeader.js +++ b/src/components/Observations/ObsListHeader.js @@ -1,25 +1,22 @@ // @flow - -import { View } from "components/styledComponents"; import type { Node } from "react"; import React from "react"; import useCurrentUser from "sharedHooks/useCurrentUser"; import LoggedOutCard from "./LoggedOutCard"; -import Toolbar from "./Toolbar"; +import ObsListToolbar from "./ObsListToolbar"; import UserCard from "./UserCard"; type Props = { - setView: Function + setLayout: Function; + layout: string } const ObsListHeader = ( { - setView + setLayout, + layout }: Props ): Node => { const currentUser = useCurrentUser( ); - if ( currentUser === null ) { - return ; - } return ( // $FlowIgnore @@ -27,7 +24,7 @@ const ObsListHeader = ( { {currentUser ? : } - + ); }; diff --git a/src/components/Observations/ObsListToolbar.js b/src/components/Observations/ObsListToolbar.js new file mode 100644 index 000000000..400480712 --- /dev/null +++ b/src/components/Observations/ObsListToolbar.js @@ -0,0 +1,159 @@ +// @flow + +import { useNavigation } from "@react-navigation/native"; +import { Pressable, Text, View } from "components/styledComponents"; +import { t } from "i18next"; +import { ObsEditContext } from "providers/contexts"; +import type { Node } from "react"; +import React, { useContext } from "react"; +import { Animated, Easing } from "react-native"; +import { ProgressBar } from "react-native-paper"; +import IconMaterial from "react-native-vector-icons/MaterialIcons"; +import useCurrentUser from "sharedHooks/useCurrentUser"; +import useLocalObservations from "sharedHooks/useLocalObservations"; +import useNumUnuploadedObservations from "sharedHooks/useNumUnuploadedObservations"; +import useUploadObservations from "sharedHooks/useUploadObservations"; +import colors from "styles/tailwindColors"; + +type Props = { + setLayout: Function, + layout: string, +}; + +const Toolbar = ( { setLayout, layout }: Props ): Node => { + const currentUser = useCurrentUser( ); + const obsEditContext = useContext( ObsEditContext ); + const { allObsToUpload } = useLocalObservations( ); + const numUnuploadedObs = useNumUnuploadedObservations( ); + const navigation = useNavigation( ); + const { + stopUpload, + uploadInProgress, + startUpload, + progress, + error: uploadError + } = useUploadObservations( allObsToUpload ); + + const spinValue = new Animated.Value( 1 ); + + Animated.timing( spinValue, { + toValue: 0, + duration: 3000, + easing: Easing.linear, + useNativeDriver: true + } ).start( ); + + const spin = spinValue.interpolate( { + inputRange: [0, 1], + outputRange: ["0deg", "360deg"] + } ); + + const loading = obsEditContext?.loading; + const syncObservations = obsEditContext?.syncObservations; + + const getSyncClick = ( ) => { + if ( numUnuploadedObs > 0 ) { + return startUpload; + } + + return syncObservations; + }; + + const getStatusText = ( ) => { + if ( !uploadInProgress && numUnuploadedObs > 0 ) { + return t( "Upload-x-observations", { count: numUnuploadedObs } ); + } + if ( numUnuploadedObs > 0 ) { + return t( "Uploading-X-Observations", { count: numUnuploadedObs } ); + } + return null; + }; + + const getSyncIconColor = ( ) => { + if ( uploadInProgress || numUnuploadedObs > 0 ) { + return colors.inatGreen; + } + return colors.darkGray; + }; + + const statusText = getStatusText( ); + /* eslint-disable react-native/no-inline-styles */ + return ( + + + {currentUser && ( + navigation.navigate( "MainStack", { screen: "Explore" } )} + > + + + )} + + + + + + + {statusText && ( + + {statusText} + {uploadError && ( + + {uploadError} + + )} + + )} + + + {uploadInProgress && ( + + + + )} + + setLayout( currentView => { + if ( currentView === "list" ) { + return "grid"; + } + return "list"; + } )} + accessibilityRole="button" + > + + + + + + + ); +}; + +export default Toolbar; diff --git a/src/components/Observations/ObservationViews.js b/src/components/Observations/ObservationViews.js index fd0e9d074..a77b60c32 100644 --- a/src/components/Observations/ObservationViews.js +++ b/src/components/Observations/ObservationViews.js @@ -26,7 +26,7 @@ const HEADER_HEIGHT = 101; const ObservationViews = ( ): Node => { const localObservations = useLocalObservations( ); - const [view, setView] = useState( "list" ); + const [layout, setLayout] = useState( "list" ); const navigation = useNavigation( ); const currentUser = useCurrentUser( ); const { observationList } = localObservations; @@ -90,7 +90,7 @@ const ObservationViews = ( ): Node => { if ( currentUser === false ) { return ; } return ( ); @@ -118,15 +118,15 @@ const ObservationViews = ( ): Node => { } + ListHeaderComponent={} ListFooterComponent={renderFooter} - ItemSeparatorComponent={view !== "grid" && renderItemSeparator} + ItemSeparatorComponent={layout !== "grid" && renderItemSeparator} stickyHeaderIndices={[0]} bounces={false} initialNumToRender={10} diff --git a/src/components/Observations/Toolbar.js b/src/components/Observations/Toolbar.js deleted file mode 100644 index 2ad883687..000000000 --- a/src/components/Observations/Toolbar.js +++ /dev/null @@ -1,55 +0,0 @@ -// @flow - -import { Pressable, View } from "components/styledComponents"; -import { ObsEditContext } from "providers/contexts"; -import type { Node } from "react"; -import React, { useContext } from "react"; -import { ActivityIndicator } from "react-native"; -import IconMaterial from "react-native-vector-icons/MaterialIcons"; -import useCurrentUser from "sharedHooks/useCurrentUser"; - -type Props = { - setView: Function - } - -const Toolbar = ( { setView }: Props ): Node => { - const currentUser = useCurrentUser( ); - const obsEditContext = useContext( ObsEditContext ); - const loading = obsEditContext?.loading; - const syncObservations = obsEditContext?.syncObservations; - - return ( - - {currentUser ? ( - - - - ) : ( - - )} - {loading && } - - setView( "list" )} - accessibilityRole="button" - > - - - setView( "grid" )} - testID="ObsList.toggleGridView" - accessibilityRole="button" - > - - - - - ); -}; - -export default Toolbar; diff --git a/src/components/Observations/UploadProgressBar.js b/src/components/Observations/UploadProgressBar.js deleted file mode 100644 index c908adb34..000000000 --- a/src/components/Observations/UploadProgressBar.js +++ /dev/null @@ -1,64 +0,0 @@ -// @flow - -import BottomSheet, { BottomSheetView } from "@gorhom/bottom-sheet"; -import { t } from "i18next"; -import type { Node } from "react"; -import React, { useMemo, useRef } from "react"; -import { - Button, ProgressBar, Text -} from "react-native-paper"; -import useNumUnuploadedObservations from "sharedHooks/useNumUnuploadedObservations"; -import { textStyles, viewStyles } from "styles/observations/uploadProgressBar"; -import colors from "styles/tailwindColors"; - -type Props = { - allObsToUpload: Array, - stopUpload: () => void -} - -const UploadProgressBar = ( { - allObsToUpload, - stopUpload -}: Props ): Node => { - const numUnuploadedObs = useNumUnuploadedObservations( ); - const totalObsToUpload = Math.max( allObsToUpload.length, numUnuploadedObs ); - - const calculateProgress = ( ) => ( totalObsToUpload - numUnuploadedObs ) / totalObsToUpload; - - const progressFraction = calculateProgress( ); - - const sheetRef = useRef( null ); - - const snapPoints = useMemo( () => ["25%"], [] ); - - // eslint-disable-next-line react/jsx-no-useless-fragment - const noHandle = ( ) => <>; - - return ( - - -