├── .circleci └── config.yml ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .github └── demo.gif ├── .gitignore ├── .prettierignore ├── .vscode ├── extensions.json └── settings.json ├── @types └── react-native.d.ts ├── LICENSE ├── README.md ├── example ├── .watchmanconfig ├── App.tsx ├── app.json ├── assets │ ├── icon.png │ └── splash.png ├── babel.config.js ├── metro.config.js ├── package.json ├── tsconfig.json └── yarn.lock ├── package.json ├── src ├── ModalizeWebView.tsx ├── index.ts └── utils │ ├── isIphoneXOrXr.ts │ └── useCombinedRefs.ts ├── tsconfig.json └── yarn.lock /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | commands: 4 | yarn_install: 5 | description: Install Javascript dependencies using Yarn. This command correctly configures the cache for any number of package.json and yarn.lock files. 6 | steps: 7 | - run: 8 | name: Create cache checksum file 9 | command: | 10 | mkdir -p ~/.tmp/checksumfiles 11 | find . -type f -name 'package.json' -not -path "*node_modules*" -exec cat {} + >> ~/.tmp/checksumfiles/package.json 12 | find . -type f -name 'yarn.lock' -not -path "*node_modules*" -exec cat {} + >> ~/.tmp/checksumfiles/yarn.lock 13 | - restore_cache: 14 | name: Restore Yarn cache 15 | keys: 16 | - yarn-cache-{{ arch }}-{{ checksum "~/.tmp/checksumfiles/package.json" }}-{{ checksum "~/.tmp/checksumfiles/yarn.lock" }}-{{ .Environment.CACHE_VERSION }} 17 | - run: 18 | name: Install dependencies with Yarn 19 | command: yarn install --non-interactive --cache-folder ~/.cache/yarn 20 | - save_cache: 21 | name: Save Yarn cache 22 | paths: 23 | - ~/.cache/yarn 24 | key: | 25 | yarn-cache-{{ arch }}-{{ checksum "~/.tmp/checksumfiles/package.json" }}-{{ checksum "~/.tmp/checksumfiles/yarn.lock" }}-{{ .Environment.CACHE_VERSION }} 26 | executors: 27 | node_10: 28 | docker: 29 | - image: circleci/node:10 30 | environment: 31 | - PATH: '/opt/yarn/yarn-v1.5.1/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin' 32 | 33 | jobs: 34 | test_node_10: 35 | executor: node_10 36 | steps: 37 | - checkout 38 | - yarn_install 39 | - run: 40 | name: Remove example directory 41 | command: rm -rf example 42 | - run: 43 | name: Run tests 44 | command: yarn test 45 | release: 46 | executor: node_10 47 | steps: 48 | - checkout 49 | - yarn_install 50 | - run: 51 | name: Release the package 52 | command: yarn semantic-release 53 | 54 | workflows: 55 | version: 2 56 | test_and_release: 57 | jobs: 58 | - test_node_10 59 | - release: 60 | requires: 61 | - test_node_10 62 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # Unignore dotfiles 2 | !.* 3 | 4 | # Node.js 5 | **/node_modules/ 6 | 7 | # Compilation artifacts 8 | /dist/ 9 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** @type {import('@alkafinance/eslint-config/eslint').ESLintConfig} */ 2 | module.exports = { 3 | extends: [ 4 | '@alkafinance/eslint-config', 5 | '@alkafinance/eslint-config-typescript', 6 | '@alkafinance/eslint-config-react/native', 7 | '@alkafinance/eslint-config-typescript/react-native', 8 | ], 9 | rules: { 10 | // https://github.com/typescript-eslint/typescript-eslint/tree/master/packages/eslint-plugin 11 | '@typescript-eslint/no-magic-numbers': 'off', 12 | // https://github.com/benmosher/eslint-plugin-import 13 | 'import/extensions': 'off', 14 | 'import/no-unresolved': 'off', 15 | // https://github.com/Intellicode/eslint-plugin-react-native 16 | 'react-native/no-color-literals': 'off', 17 | }, 18 | overrides: [ 19 | { 20 | files: ['*.js', '**/.*.js'], 21 | ...require('@alkafinance/eslint-config/script'), 22 | }, 23 | ], 24 | }; 25 | -------------------------------------------------------------------------------- /.github/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alkafinance/react-native-modalize-webview/0ee69e1a22eb2bf1255b9f99622f930cc2c9475e/.github/demo.gif -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # macOS 2 | **/.DS_Store 3 | 4 | # Node.js 5 | **/node_modules/ 6 | **/.yarn-integrity 7 | 8 | # Logs 9 | /logs/ 10 | **/*.log 11 | **/npm-debug.log* 12 | **/yarn-debug.log* 13 | **/yarn-error.log* 14 | 15 | # Compilation artifacts 16 | /dist/ 17 | /example/.expo/ 18 | 19 | # ESLint 20 | /.eslintcache 21 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Node.js 2 | **/node_modules/ 3 | **/package.json 4 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "editorconfig.editorconfig", 5 | "esbenp.prettier-vscode" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": false, 3 | "editor.insertSpaces": true, 4 | "editor.rulers": [80], 5 | "editor.tabSize": 2, 6 | "[javascript]": { 7 | "editor.formatOnSave": true 8 | }, 9 | "[javascriptreact]": { 10 | "editor.formatOnSave": true 11 | }, 12 | "[typescript]": { 13 | "editor.formatOnSave": true 14 | }, 15 | "[typescriptreact]": { 16 | "editor.formatOnSave": true 17 | }, 18 | "[json]": { 19 | "editor.formatOnSave": true 20 | }, 21 | "[jsonc]": { 22 | "editor.formatOnSave": true 23 | }, 24 | "[yaml]": { 25 | "editor.formatOnSave": true 26 | }, 27 | 28 | "files.exclude": { 29 | "**/.git/": true, 30 | "**/.DS_Store": true, 31 | ".eslintcache": true 32 | }, 33 | "search.exclude": { 34 | "**/node_modules/": true 35 | }, 36 | 37 | "eslint.nodePath": "./node_modules", 38 | "eslint.options": { 39 | "extensions": [".js", ".ts", ".tsx"], 40 | "rules": { 41 | "padding-line-between-statements": "off" 42 | } 43 | }, 44 | "eslint.validate": [ 45 | "javascript", 46 | "javascriptreact", 47 | { 48 | "language": "typescript", 49 | "autoFix": true 50 | }, 51 | { 52 | "language": "typescriptreact", 53 | "autoFix": true 54 | } 55 | ], 56 | 57 | "javascript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces": false, 58 | "javascript.implicitProjectConfig.checkJs": true, 59 | "javascript.suggestionActions.enabled": false, 60 | "javascript.validate.enable": true, 61 | "typescript.tsdk": "./node_modules/typescript/lib", 62 | "typescript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces": false, 63 | "typescript.disableAutomaticTypeAcquisition": true, 64 | "typescript.format.enable": false, 65 | "typescript.preferences.quoteStyle": "single", 66 | "typescript.preferences.renameShorthandProperties": true, 67 | 68 | "emmet.showExpandedAbbreviation": "never" 69 | } 70 | -------------------------------------------------------------------------------- /@types/react-native.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'react-native/Libraries/vendor/emitter/EventEmitter' { 2 | import {EventEmitter} from 'react-native'; 3 | 4 | const EventEmitter: EventEmitter; 5 | 6 | export = EventEmitter; 7 | } 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Alka, Inc (https://alka.app) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-native-modalize-webview 2 | 3 | [![npm version](https://img.shields.io/npm/v/react-native-modalize-webview.svg)](https://www.npmjs.org/package/react-native-modalize-webview) 4 | [![CircleCI Status](https://img.shields.io/circleci/project/github/alkafinance/react-native-modalize-webview/master.svg)](https://circleci.com/gh/alkafinance/workflows/react-native-modalize-webview/tree/master) 5 | ![license: MIT](https://img.shields.io/npm/l/react-native-modalize-webview.svg) 6 | ![Supports iOS](https://img.shields.io/badge/platforms-ios-lightgrey.svg) 7 | [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)](https://github.com/prettier/prettier) 8 | [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release) 9 | 10 | React Native modal component that brings swipe to dismiss to WebView. 11 | 12 | 13 | 14 | ## Getting started 15 | 16 | `$ npm install react-native-modalize-webview --save` 17 | 18 | ## Usage 19 | 20 | Import `ModalizeWebView` and use it like the regular [`Modalize` component](https://github.com/jeremybarbet/react-native-modalize/). Provide `WebView` props via `webViewProps` to customize the displayed web page. 21 | 22 | ```javascript 23 | import {ModalizeWebView} from 'react-native-modalize-webview' 24 | 25 | function MyComponent() { 26 | const modalizeRef = useRef(null) 27 | 28 | const handleOpen = useCallback(() => { 29 | if (modalizeRef.current) { 30 | modalizeRef.current.open() 31 | } 32 | }, []) 33 | 34 | return ( 35 | <> 36 | 37 | Open the modal 38 | 39 | 40 | 49 | 50 | ) 51 | } 52 | ``` 53 | 54 | ## Props 55 | 56 | - [Inherited `Modalize` props...](https://jeremybarbet.github.io/react-native-modalize/#/PROPSMETHODS) 57 | 58 | - [`webViewProps`](#webViewProps) 59 | - [`anchorOffset`](#anchorOffset) 60 | 61 | --- 62 | 63 | # Reference 64 | 65 | ## Props 66 | 67 | ### `webViewProps` 68 | 69 | Configures the underlying `WebView` component. 70 | 71 | | Type | Required | 72 | | -------------------------------------------------------------------------------------------------------------------- | -------- | 73 | | [`WebViewProps`](https://github.com/react-native-community/react-native-webview/blob/master/docs/Reference.md#props) | Yes | 74 | 75 | ### `anchorOffset` 76 | 77 | Specifies extra offset from top on scroll to an anchor link. Defaults to `16`. 78 | 79 | | Type | Required | 80 | | -------- | -------- | 81 | | `number` | No | 82 | 83 | ## License 84 | 85 | [MIT License](./LICENSE) © Alka, Inc 86 | -------------------------------------------------------------------------------- /example/.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /example/App.tsx: -------------------------------------------------------------------------------- 1 | import React, {useRef} from 'react' 2 | import {SafeAreaView, StyleSheet, View} from 'react-native' 3 | import Modalize from 'react-native-modalize' 4 | import {ActivityIndicator, Button, Colors, Headline} from 'react-native-paper' 5 | import {ModalizeWebView} from '../src' 6 | 7 | export default function App() { 8 | const modalizeRef = useRef(null) 9 | 10 | return ( 11 | 12 | 13 | 20 | 21 | 22 | ( 32 | 33 | 34 | 35 | ), 36 | renderError: () => ( 37 | 38 | Something went wrong 39 | 40 | ), 41 | }} 42 | /> 43 | 44 | ) 45 | } 46 | 47 | const styles = StyleSheet.create({ 48 | container: { 49 | flex: 1, 50 | backgroundColor: Colors.white, 51 | }, 52 | buttons: { 53 | flex: 1, 54 | justifyContent: 'center', 55 | paddingHorizontal: 16, 56 | }, 57 | fullscreen: { 58 | ...StyleSheet.absoluteFillObject, 59 | backgroundColor: Colors.white, 60 | justifyContent: 'center', 61 | alignItems: 'center', 62 | }, 63 | }) 64 | -------------------------------------------------------------------------------- /example/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "Example", 4 | "slug": "example", 5 | "privacy": "public", 6 | "sdkVersion": "33.0.0", 7 | "platforms": ["ios", "android", "web"], 8 | "version": "1.0.0", 9 | "orientation": "portrait", 10 | "icon": "./assets/icon.png", 11 | "splash": { 12 | "image": "./assets/splash.png", 13 | "resizeMode": "contain", 14 | "backgroundColor": "#ffffff" 15 | }, 16 | "updates": { 17 | "fallbackToCacheTimeout": 0 18 | }, 19 | "assetBundlePatterns": ["**/*"], 20 | "ios": { 21 | "supportsTablet": true 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /example/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alkafinance/react-native-modalize-webview/0ee69e1a22eb2bf1255b9f99622f930cc2c9475e/example/assets/icon.png -------------------------------------------------------------------------------- /example/assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alkafinance/react-native-modalize-webview/0ee69e1a22eb2bf1255b9f99622f930cc2c9475e/example/assets/splash.png -------------------------------------------------------------------------------- /example/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = api => { 2 | api.cache(true); 3 | 4 | return { 5 | presets: ['babel-preset-expo'], 6 | }; 7 | }; 8 | -------------------------------------------------------------------------------- /example/metro.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | resolver: { 5 | extraNodeModules: { 6 | '@babel/runtime': path.resolve(__dirname, 'node_modules/@babel/runtime'), 7 | react: path.resolve(__dirname, 'node_modules/react'), 8 | 'react-native': path.resolve(__dirname, 'node_modules/react-native'), 9 | 'react-native-modalize': path.resolve( 10 | __dirname, 11 | 'node_modules/react-native-modalize', 12 | ), 13 | 'react-native-webview': path.resolve( 14 | __dirname, 15 | 'node_modules/react-native-webview', 16 | ), 17 | }, 18 | }, 19 | watchFolders: [path.resolve(__dirname, '../src')], 20 | }; 21 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "node_modules/expo/AppEntry.js", 3 | "scripts": { 4 | "start": "expo start", 5 | "android": "expo start --android", 6 | "ios": "expo start --ios", 7 | "web": "expo start --web", 8 | "eject": "expo eject" 9 | }, 10 | "dependencies": { 11 | "expo": "^33.0.0", 12 | "faker": "4.1.0", 13 | "react": "16.8.3", 14 | "react-dom": "^16.8.6", 15 | "react-native": "https://github.com/expo/react-native/archive/sdk-33.0.0.tar.gz", 16 | "react-native-keyboard-avoiding-scroll-view": "^1.0.1", 17 | "react-native-modalize": "1.2.0", 18 | "react-native-paper": "^2.16.0", 19 | "react-native-web": "^0.11.4", 20 | "react-native-webview": "~5.8.1" 21 | }, 22 | "devDependencies": { 23 | "@types/react": "16.8.23", 24 | "@types/react-native": "0.60.2", 25 | "babel-preset-expo": "^5.1.1", 26 | "expo-cli": "^3.0.4", 27 | "typescript": "^3.4.5" 28 | }, 29 | "private": true 30 | } 31 | -------------------------------------------------------------------------------- /example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "include": ["**/*"] 4 | } 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-modalize-webview", 3 | "version": "0.0.0-development", 4 | "description": "React Native modal component that brings swipe to dismiss to WebView", 5 | "keywords": [ 6 | "react native", 7 | "modalize", 8 | "modal", 9 | "webview", 10 | "alka" 11 | ], 12 | "homepage": "https://github.com/alkafinance/react-native-modalize-webview#readme", 13 | "bugs": { 14 | "url": "https://github.com/alkafinance/react-native-modalize-webview/issues" 15 | }, 16 | "license": "MIT", 17 | "author": "Ayan Yenbekbay ", 18 | "files": [ 19 | "dist/" 20 | ], 21 | "main": "dist/index.js", 22 | "react-native": "dist/index.js", 23 | "types": "dist/index.d.ts", 24 | "sideEffects": false, 25 | "repository": { 26 | "type": "git", 27 | "url": "https://github.com/alkafinance/react-native-modalize-webview.git" 28 | }, 29 | "publishConfig": { 30 | "registry": "https://registry.npmjs.org/" 31 | }, 32 | "scripts": { 33 | "typecheck": "tsc --noEmit --pretty", 34 | "lint": "eslint --ext .js,.ts,.tsx --cache .", 35 | "test": "run-p --silent --print-label typecheck lint", 36 | "bootstrap": "yarn --cwd example && yarn", 37 | "build": "rm -rf dist && yarn tsc --pretty --declaration --outDir dist", 38 | "example": "yarn --cwd example", 39 | "prepublishOnly": "yarn build", 40 | "presemantic-release": "yarn build", 41 | "semantic-release": "semantic-release" 42 | }, 43 | "prettier": { 44 | "bracketSpacing": false, 45 | "jsxBracketSameLine": true, 46 | "printWidth": 80, 47 | "semi": false, 48 | "singleQuote": true, 49 | "tabWidth": 2, 50 | "trailingComma": "all", 51 | "useTabs": false 52 | }, 53 | "husky": { 54 | "hooks": { 55 | "pre-commit": "lint-staged", 56 | "commit-msg": "commitlint -e", 57 | "pre-push": "yarn test" 58 | } 59 | }, 60 | "lint-staged": { 61 | "**/*.{js,ts,tsx,json,yml,yaml}": [ 62 | "prettier --write", 63 | "git add" 64 | ], 65 | "**/*.{js,ts,tsx}": [ 66 | "eslint --ext .js,.ts,.tsx --cache --fix", 67 | "git add" 68 | ] 69 | }, 70 | "commitlint": { 71 | "extends": [ 72 | "@commitlint/config-conventional" 73 | ] 74 | }, 75 | "peerDependencies": { 76 | "react": "*", 77 | "react-native": "*", 78 | "react-native-modalize": "1.2.0", 79 | "react-native-webview": "*" 80 | }, 81 | "dependencies": {}, 82 | "devDependencies": { 83 | "@alkafinance/eslint-config": "^1.0.1", 84 | "@alkafinance/eslint-config-react": "^1.0.0", 85 | "@alkafinance/eslint-config-typescript": "^1.0.0", 86 | "@commitlint/cli": "^8.1.0", 87 | "@commitlint/config-conventional": "^8.1.0", 88 | "@types/react": "16.8.23", 89 | "@types/react-native": "0.60.2", 90 | "eslint": "5.10.0", 91 | "husky": "^3.0.1", 92 | "lint-staged": "^9.2.1", 93 | "npm-run-all": "^4.1.5", 94 | "prettier": "^1.18.2", 95 | "react-native-modalize": "1.2.0", 96 | "react-native-webview": "~5.8.1", 97 | "semantic-release": "^15.13.18", 98 | "typescript": "^3.5.3", 99 | "utility-types": "^3.7.0" 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/ModalizeWebView.tsx: -------------------------------------------------------------------------------- 1 | import React, {useCallback, useMemo, useRef, useState} from 'react' 2 | import { 3 | Dimensions, 4 | NativeSyntheticEvent, 5 | Platform, 6 | StatusBar, 7 | } from 'react-native' 8 | import Modalize from 'react-native-modalize' 9 | import WebView, {WebViewProps} from 'react-native-webview' 10 | import { 11 | WebViewMessage, 12 | WebViewNavigation, 13 | } from 'react-native-webview/lib/WebViewTypes' 14 | import {isIphoneXOrXr} from './utils/isIphoneXOrXr' 15 | import {useCombinedRefs} from './utils/useCombinedRefs' 16 | 17 | const {height: SCREEN_HEIGHT} = Dimensions.get('window') 18 | const STATUS_BAR_HEIGHT = Platform.select({ 19 | ios: isIphoneXOrXr() ? 44 : 20, 20 | android: StatusBar.currentHeight, 21 | }) 22 | const NAVIGATION_BAR_HEIGHT = Platform.select({ 23 | ios: 44, 24 | android: 56, 25 | }) 26 | 27 | // Based on https://github.com/jeremybarbet/react-native-modalize/blob/1.2.0/src/Modalize.tsx#L53 28 | const MODAL_CONTAINER_HEIGHT = 29 | Platform.OS === 'ios' ? SCREEN_HEIGHT : SCREEN_HEIGHT - 10 30 | 31 | export interface ModalizeWebViewProps 32 | extends React.ComponentProps { 33 | webViewProps: Omit 34 | anchorOffset?: number 35 | } 36 | 37 | export const ModalizeWebView = React.forwardRef( 38 | ( 39 | {webViewProps, anchorOffset = 16, ...modalizeProps}: ModalizeWebViewProps, 40 | ref: React.Ref, 41 | ) => { 42 | // eslint-disable-next-line no-underscore-dangle 43 | const _modalizeRef = useRef(null) 44 | const modalizeRef = useCombinedRefs(ref, _modalizeRef) 45 | const webViewRef = useRef(null) 46 | 47 | const contentExpandedHeight = useMemo( 48 | () => 49 | MODAL_CONTAINER_HEIGHT - 50 | STATUS_BAR_HEIGHT - 51 | (modalizeProps.handlePosition === 'outside' ? 35 : 0), 52 | [modalizeProps.handlePosition], 53 | ) 54 | const contentCollapsedHeight = useMemo( 55 | () => contentExpandedHeight - NAVIGATION_BAR_HEIGHT, 56 | [contentExpandedHeight], 57 | ) 58 | const [documentHeight, setDocumentHeight] = useState(contentExpandedHeight) 59 | 60 | const handleNavigationStateChange = useCallback( 61 | (event: WebViewNavigation) => { 62 | if (webViewProps.onNavigationStateChange) { 63 | webViewProps.onNavigationStateChange(event) 64 | } 65 | 66 | if (!event.loading && !event.navigationType) { 67 | setDocumentHeight(contentExpandedHeight) 68 | // Wait for the page to actually load, otherwise we will inject 69 | // the script into the previous page 70 | setTimeout(() => { 71 | // HACK: Use the document height of the page as the actual height 72 | // of the view to fix dismiss gesture 73 | if (webViewRef.current) { 74 | webViewRef.current.injectJavaScript(documentHeightCallbackScript) 75 | } 76 | }, 500) 77 | } else if ( 78 | !event.loading && 79 | event.navigationType === 'click' && 80 | // Test if the url is an anchor 81 | /#[a-zA-Z][\w:.-]*$/.test(event.url) 82 | ) { 83 | // HACK: Use the anchor tag from the url to run a custom 84 | // JavaScript script that finds the element offset from top. 85 | // Then use that offset to actually scroll to the element 86 | // mimicking native web behaviour 87 | if (webViewRef.current) { 88 | webViewRef.current.injectJavaScript( 89 | elementByIdOffsetTopCallbackScript, 90 | ) 91 | } 92 | } 93 | }, 94 | [contentExpandedHeight, webViewProps], 95 | ) 96 | const handleMessage = useCallback( 97 | (event: NativeSyntheticEvent) => { 98 | if (webViewProps.onMessage) { 99 | webViewProps.onMessage(event) 100 | } 101 | 102 | const data: 103 | | { 104 | event: 'documentHeight' 105 | documentHeight: number 106 | } 107 | | { 108 | event: 'elementByIdOffsetTop' 109 | offsetTop: number 110 | } 111 | | null 112 | | undefined = JSON.parse(event.nativeEvent.data) 113 | if (!data) return 114 | 115 | switch (data.event) { 116 | case 'documentHeight': { 117 | if (data.documentHeight !== 0) { 118 | setDocumentHeight(data.documentHeight) 119 | } 120 | 121 | break 122 | } 123 | case 'elementByIdOffsetTop': { 124 | if (_modalizeRef.current) { 125 | _modalizeRef.current.scrollTo({ 126 | y: Math.max( 127 | 0, 128 | data.offsetTop - 129 | (modalizeProps.handlePosition === 'inside' ? 20 : 0) - 130 | anchorOffset, 131 | ), 132 | animated: true, 133 | }) 134 | } 135 | 136 | break 137 | } 138 | } 139 | }, 140 | [anchorOffset, modalizeProps.handlePosition, webViewProps], 141 | ) 142 | 143 | return ( 144 | 155 | 163 | 164 | ) 165 | }, 166 | ) 167 | 168 | const documentHeightCallbackScript = ` 169 | function onElementHeightChange(elm, callback) { 170 | var lastHeight 171 | var newHeight 172 | 173 | ;(function run() { 174 | newHeight = Math.max(elm.clientHeight, elm.scrollHeight) 175 | if (lastHeight != newHeight) { 176 | callback(newHeight) 177 | } 178 | lastHeight = newHeight 179 | 180 | if (elm.onElementHeightChangeTimer) { 181 | clearTimeout(elm.onElementHeightChangeTimer) 182 | } 183 | 184 | elm.onElementHeightChangeTimer = setTimeout(run, 200) 185 | })() 186 | } 187 | 188 | onElementHeightChange(document.body, function(height) { 189 | window.ReactNativeWebView.postMessage( 190 | JSON.stringify({ 191 | event: 'documentHeight', 192 | documentHeight: height, 193 | }), 194 | ) 195 | }) 196 | ` 197 | 198 | const elementByIdOffsetTopCallbackScript = ` 199 | // Based on https://stackoverflow.com/a/1480137 200 | function cumulativeOffset(elm) { 201 | var top = 0 202 | var left = 0 203 | do { 204 | top += elm.offsetTop || 0 205 | left += elm.offsetLeft || 0 206 | elm = elm.offsetParent 207 | } while (elm) 208 | 209 | return { 210 | top: top, 211 | left: left, 212 | } 213 | } 214 | 215 | if (window.location.hash) { 216 | var id = window.location.hash.replace('#', '') 217 | var elm = document.getElementById(id) 218 | if (!elm) { 219 | // Special case for GitHub 220 | elm = document.getElementById('user-content-' + id) 221 | } 222 | 223 | if (elm) { 224 | window.ReactNativeWebView.postMessage( 225 | JSON.stringify({ 226 | event: 'elementByIdOffsetTop', 227 | offsetTop: cumulativeOffset(elm).top, 228 | }), 229 | ) 230 | } 231 | } 232 | ` 233 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ModalizeWebView' 2 | -------------------------------------------------------------------------------- /src/utils/isIphoneXOrXr.ts: -------------------------------------------------------------------------------- 1 | import {Platform, Dimensions} from 'react-native' 2 | 3 | const IPHONE_X_WIDTH = 375 4 | const IPHONE_X_HEIGHT = 812 5 | const IPHONE_XR_WIDTH = 414 6 | const IPHONE_XR_HEIGHT = 896 7 | 8 | const {width: screenWidth, height: screenHeight} = Dimensions.get('window') 9 | 10 | export const isIphoneXOrXr = () => 11 | (Platform.OS === 'ios' && 12 | ((screenHeight === IPHONE_X_HEIGHT && screenWidth === IPHONE_X_WIDTH) || 13 | (screenHeight === IPHONE_X_WIDTH && screenWidth === IPHONE_X_HEIGHT))) || 14 | ((screenHeight === IPHONE_XR_HEIGHT && screenWidth === IPHONE_XR_WIDTH) || 15 | (screenHeight === IPHONE_XR_WIDTH && screenWidth === IPHONE_XR_HEIGHT)) 16 | -------------------------------------------------------------------------------- /src/utils/useCombinedRefs.ts: -------------------------------------------------------------------------------- 1 | import React, {useCallback, MutableRefObject} from 'react' 2 | 3 | /** Based on https://github.com/facebook/react/issues/13029#issuecomment-497641073 */ 4 | export function useCombinedRefs(...refs: Array>): React.Ref { 5 | return useCallback( 6 | (element: T) => 7 | refs.forEach(ref => { 8 | if (!ref) { 9 | return 10 | } 11 | 12 | // Ref can have two types - a function or an object. We treat each case. 13 | if (typeof ref === 'function') { 14 | return ref(element) 15 | } 16 | 17 | // As per https://github.com/facebook/react/issues/13029 18 | // it should be fine to set current this way. 19 | // eslint-disable-next-line no-param-reassign 20 | ;(ref as MutableRefObject).current = element 21 | }), 22 | // eslint-disable-next-line react-hooks/exhaustive-deps 23 | refs, 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "forceConsistentCasingInFileNames": true, 5 | "jsx": "react-native", 6 | "lib": ["es2017", "esnext.asynciterable"], 7 | "module": "esnext", 8 | "moduleResolution": "node", 9 | "noFallthroughCasesInSwitch": true, 10 | "noImplicitAny": true, 11 | "noImplicitReturns": true, 12 | "noImplicitThis": true, 13 | "noUnusedLocals": true, 14 | "noUnusedParameters": true, 15 | "resolveJsonModule": true, 16 | "strictFunctionTypes": true, 17 | "strictNullChecks": true, 18 | "strictPropertyInitialization": true, 19 | "target": "esnext" 20 | }, 21 | "include": ["./@types/**/*", "./src/**/*"] 22 | } 23 | --------------------------------------------------------------------------------