├── .gitignore ├── README.md ├── package.json ├── public ├── favicon.ico ├── index.html └── manifest.json ├── src ├── App.css ├── App.js ├── App.test.js ├── components │ ├── DatePicker.js │ ├── Form.js │ ├── FruitPicker.css │ ├── FruitPicker.js │ ├── FullScreenFive.js │ ├── FullScreenFour.css │ ├── FullScreenFour.js │ ├── FullScreenOne.css │ ├── FullScreenOne.js │ ├── FullScreenSix.js │ ├── FullScreenThree.js │ ├── FullScreenTwo.css │ ├── FullScreenTwo.js │ └── fruits.js ├── index.js └── registerServiceWorker.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fullscreen mobile modal. How hard can it be? 2 | 3 | [Post on dev.to](https://dev.to/stereobooster/fullscreen-mobile-modal-how-hard-can-it-be-7mj). 4 | 5 | Imagine you need to implement a mobile-friendly form, beyond traditional inputs you need to implement fullscreen infinite calendar and a fullscreen combo box. Let's talk about "fullscreen". How hard you expect it would be? Should be not hard - just use fullscreen div with z-index and fixed position. Right? That what I thought. 6 | 7 | ## Attempt number 1 8 | 9 | [Online example](https://stereobooster.github.io/react-modal-experiment/) 10 | 11 | Let's use `div` which will cover all screen 12 | 13 | ```css 14 | .FullScreenOne { 15 | position: fixed; 16 | bottom: 0; 17 | left: 0; 18 | right: 0; 19 | top: 0; 20 | z-index: 1; 21 | background: #fff; 22 | } 23 | ``` 24 | 25 | It works, except user can use Tab (or and in iOS) to move cursor out of modal. 26 | 27 | 28 | 29 | ## Attempt number 2 30 | 31 | [Online example](https://stereobooster.github.io/react-modal-experiment/v2/) 32 | 33 | Use [Reach `Dialog`](https://ui.reach.tech/dialog) (in addition to previous solution). `Dialog` will set `aria-hidden` on all nodes at the `document.body` root except for the currently active dialog. This traps the virtual cursor inside the dialog. 34 | 35 | ~~Side note: I tried [react-focus-lock](https://github.com/theKashey/react-focus-lock), but it doesn't work in iOS (I mean for and ).~~ 36 | 37 | It works, except when the user wants to scroll the content of the modal, but instead, they will scroll content behind the modal. 38 | 39 | 40 | 41 | ## Attempt number 3 42 | 43 | [Online example](https://stereobooster.github.io/react-modal-experiment/v3/) 44 | 45 | Use [`ScrollLocky`](https://github.com/theKashey/react-scroll-locky) (in addition to previous solution). `ScrollLocky` blocks any interactions with the rest of a page with the help of `react-locky` and `position: relative` on the body. 46 | 47 | It works, except when the user scrolls down and bottom chrome of the Mobile Safari get's hidden, but later it is possible to trigger the appearance of bottom chrome and part of the modal will be hidden. 48 | 49 | 50 | 51 | ## Attempt number 4 52 | 53 | [Online example](https://stereobooster.github.io/react-modal-experiment/v4/) 54 | 55 | Use [`mobile-safari-fullscreen`](https://github.com/stereobooster/mobile-safari-fullscreen) (in addition to previous solution). It will always force to show the bottom of browser chrome when modal is opened. 56 | 57 | 58 | 59 | Side note: `WindowSize` can be used instead `mobile-safari-fullscreen`. I would say this is the preferred solution, this demo is more to show how much edge case is possible. 60 | 61 | ```js 62 | 63 | {({ height }) => ( 64 | 67 | )} 68 | 69 | ``` 70 | 71 | ## Conclusion 72 | 73 | This kind of small details is what makes frontend development hard and interesting same time. If you liked this post read [my post about the UX of images on the web](https://github.com/stereobooster/react-ideal-image/blob/master/introduction.md). 74 | 75 | **PS** [Different browser have the different behavior of focus](https://gist.github.com/cvrebert/68659d0333a578d75372). The most noticeable is Mobile Safari which doesn't allow to focus on links and buttons `¯\_(ツ)_/¯`. 76 | 77 | Check out [this GitHub repo](https://github.com/stereobooster/react-modal-experiment) for the full code for this post. 78 | 79 | Follow me on [twitter](https://twitter.com/stereobooster) and [github](https://github.com/stereobooster). -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-modal-experiment", 3 | "version": "0.1.0", 4 | "private": true, 5 | "homepage": "https://stereobooster.github.io/react-modal-experiment/", 6 | "dependencies": { 7 | "@blacklane/react-infinite-calendar": "^2.3.17", 8 | "@reach/component-component": "^0.1.1", 9 | "@reach/dialog": "^0.1.1", 10 | "@reach/rect": "^0.1.1", 11 | "@reach/router": "^1.1.1", 12 | "@reach/window-size": "^0.1.1", 13 | "downshift": "^2.2.1", 14 | "mobile-safari-fullscreen": "^0.0.1", 15 | "normalize.css": "^8.0.0", 16 | "react": "^16.4.2", 17 | "react-dom": "^16.4.2", 18 | "react-focus-lock": "^1.13.2", 19 | "react-pose": "^3.3.0", 20 | "react-scripts": "1.1.5", 21 | "react-scroll-locky": "^1.1.2" 22 | }, 23 | "devDependencies": { 24 | "gh-pages": "^1.2.0", 25 | "react-snap": "^1.19.0" 26 | }, 27 | "scripts": { 28 | "start": "react-scripts start", 29 | "build": "react-scripts build", 30 | "test": "react-scripts test --env=jsdom", 31 | "eject": "react-scripts eject", 32 | "predeploy": "yarn build", 33 | "deploy": "gh-pages -d ./build", 34 | "postbuild": "react-snap" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stereobooster/react-modal-experiment/7138e59d5691cce2885e99427bece95b2b1c8e91/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | React App 23 | 24 | 25 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | :focus { 2 | outline: none; 3 | border-color: #2188ff; 4 | box-shadow: 0 0 0 0.2em #c8e1ff; 5 | } 6 | [tabindex="-1"]:focus { 7 | outline: none; 8 | border: none; 9 | box-shadow: none; 10 | } 11 | 12 | * { 13 | box-sizing: border-box; 14 | } 15 | 16 | html { 17 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, 18 | Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; 19 | line-height: 1.15; 20 | -moz-osx-font-smoothing: grayscale; 21 | -webkit-font-smoothing: antialiased; 22 | -webkit-tap-highlight-color: transparent; 23 | -webkit-text-size-adjust: 100%; 24 | -ms-text-size-adjust: 100%; 25 | -ms-overflow-style: scrollbar; 26 | } 27 | 28 | a { 29 | text-decoration-skip: ink; 30 | } 31 | 32 | nav { 33 | padding: 0 0 1rem; 34 | } 35 | 36 | .content { 37 | padding: 1rem; 38 | } 39 | 40 | .field { 41 | background: lightblue; 42 | padding: 1rem; 43 | margin-bottom: 0.5rem; 44 | } 45 | 46 | .spacer { 47 | margin-bottom: 20rem; 48 | } 49 | 50 | .close { 51 | height: 30px; 52 | -webkit-appearance: initial; 53 | float: right; 54 | } 55 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./App.css"; 3 | import { Router, Link, Redirect } from "@reach/router"; 4 | import Form from "./components/Form"; 5 | import FullScreenOne from "./components/FullScreenOne"; 6 | import FullScreenTwo from "./components/FullScreenTwo"; 7 | import FullScreenThree from "./components/FullScreenThree"; 8 | import FullScreenFour from "./components/FullScreenFour"; 9 | import FullScreenFive from "./components/FullScreenFive"; 10 | 11 | const path = "react-modal-experiment"; 12 | 13 | class App extends React.Component { 14 | render() { 15 | return ( 16 |
17 | 23 | 24 | 25 | 26 |
27 | 32 | 37 | 42 | 47 | 48 |
49 | ); 50 | } 51 | } 52 | 53 | export default App; 54 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /src/components/DatePicker.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Component from "@reach/component-component"; 3 | import InfiniteCalendar from "@blacklane/react-infinite-calendar"; 4 | import "@blacklane/react-infinite-calendar/styles.css"; 5 | import WindowSize from "@reach/window-size"; 6 | 7 | export default ({ FullScreenModal }) => ( 8 | 9 | {({ state, setState }) => ( 10 |
11 |
12 | {" "} 15 | {`${state.value.getDate()}.${state.value.getMonth()}.${state.value.getFullYear()}`} 16 |
17 | {state.showDialog && ( 18 | 19 | 25 | 26 | {size => ( 27 | setState({ showDialog: false, value })} 43 | tabIndex={-1} 44 | /> 45 | )} 46 | 47 | 48 | )} 49 |
50 | )} 51 |
52 | ); 53 | -------------------------------------------------------------------------------- /src/components/Form.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import DatePicker from "./DatePicker"; 3 | import FruitPicker from "./FruitPicker"; 4 | 5 | export default ({ FullScreenModal, version }) => ( 6 |
7 | 8 |

Version: {version}

9 |
10 | {" "} 11 | 12 |
13 | 14 | 15 |

Scroll down

16 | 17 |

18 |

19 | ); 20 | -------------------------------------------------------------------------------- /src/components/FruitPicker.css: -------------------------------------------------------------------------------- 1 | .fruitInput { 2 | width: calc(100% - 2rem); 3 | margin: 0 1rem; 4 | height: 30px; 5 | } 6 | .fruitList { 7 | margin: 0; 8 | padding: 0; 9 | overflow: scroll; 10 | height: calc(100vh - 30px - 30px); 11 | } 12 | 13 | .FullScreenFour .fruitList { 14 | height: calc(100vh - 30px - 30px - 70px); 15 | } 16 | 17 | .fruitItem { 18 | list-style: none; 19 | margin: 0; 20 | padding: 1rem; 21 | border-bottom: 1px solid lightgrey; 22 | } 23 | -------------------------------------------------------------------------------- /src/components/FruitPicker.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Component from "@reach/component-component"; 3 | import Downshift from "downshift"; 4 | import "./FruitPicker.css"; 5 | import fruits from "./fruits"; 6 | 7 | const stateReducer = (state, changes) => { 8 | switch (changes.type) { 9 | case Downshift.stateChangeTypes.blurInput: 10 | return { 11 | ...changes, 12 | inputValue: state.inputValue 13 | }; 14 | default: 15 | return changes; 16 | } 17 | }; 18 | 19 | export default ({ FullScreenModal }) => ( 20 | 21 | {({ state, setState }) => ( 22 |
23 |
24 | {" "} 27 | {state.value} 28 |
29 | {state.showDialog && ( 30 | 31 | 38 | setState({ showDialog: false, value })} 40 | itemToString={item => (item ? item.value : "")} 41 | stateReducer={stateReducer} 42 | selectedItem={{ value: state.value }} 43 | > 44 | {({ 45 | getInputProps, 46 | getItemProps, 47 | getMenuProps, 48 | inputValue, 49 | highlightedIndex, 50 | selectedItem 51 | }) => ( 52 |
53 | 59 |
    60 | {inputValue 61 | ? fruits 62 | .filter(item => 63 | item.value.includes(inputValue.toLowerCase()) 64 | ) 65 | .map((item, index) => ( 66 |
  • 82 | {item.value} 83 |
  • 84 | )) 85 | : null} 86 |
87 |
88 | )} 89 |
90 |
91 | )} 92 |
93 | )} 94 |
95 | ); 96 | -------------------------------------------------------------------------------- /src/components/FullScreenFive.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./FullScreenOne.css"; 3 | import { ScrollLocky } from "react-scroll-locky"; 4 | import FocusLock from "react-focus-lock"; 5 | 6 | export default ({ children }) => ( 7 | 8 | 9 |
{children}
10 |
11 |
12 | ); 13 | -------------------------------------------------------------------------------- /src/components/FullScreenFour.css: -------------------------------------------------------------------------------- 1 | .iosFix { 2 | /* Allows content to fill the viewport and go beyond the bottom */ 3 | height: 100%; 4 | /* Allows you to scroll below the viewport; default value is visible */ 5 | overflow-y: scroll; 6 | /* To smooth any scrolling behavior */ 7 | -webkit-overflow-scrolling: touch; 8 | } 9 | 10 | .FullScreenFour { 11 | bottom: 70px; 12 | } 13 | -------------------------------------------------------------------------------- /src/components/FullScreenFour.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./FullScreenOne.css"; 3 | import "./FullScreenTwo.css"; 4 | import "./FullScreenFour.css"; 5 | 6 | import { Dialog } from "@reach/dialog"; 7 | import "@reach/dialog/styles.css"; 8 | 9 | import { ScrollLocky } from "react-scroll-locky"; 10 | 11 | import FullScreen from "mobile-safari-fullscreen"; 12 | 13 | export default ({ children, show }) => ( 14 | 15 | 16 | {children} 17 | 18 | 19 | ); 20 | -------------------------------------------------------------------------------- /src/components/FullScreenOne.css: -------------------------------------------------------------------------------- 1 | body, html { 2 | height: 100%; 3 | } 4 | 5 | .FullScreenOne { 6 | position: fixed; 7 | bottom: 0; 8 | left: 0; 9 | right: 0; 10 | top: 0; 11 | z-index: 1; 12 | background: #fff; 13 | } 14 | -------------------------------------------------------------------------------- /src/components/FullScreenOne.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./FullScreenOne.css"; 3 | 4 | export default ({ children }) => ( 5 |
{children}
6 | ); 7 | -------------------------------------------------------------------------------- /src/components/FullScreenSix.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./FullScreenOne.css"; 3 | import { ScrollLocky } from "react-scroll-locky"; 4 | import FocusLock from "react-focus-lock"; 5 | import posed from "react-pose"; 6 | 7 | const Box = posed.div({ 8 | zoomedIn: { 9 | position: "fixed", 10 | top: 0, 11 | left: 0, 12 | bottom: 0, 13 | right: 0, 14 | flip: true, 15 | opacity: 1 16 | }, 17 | zoomedOut: { 18 | position: "static", 19 | width: "auto", 20 | height: "auto", 21 | flip: true, 22 | opacity: 0 23 | } 24 | }); 25 | 26 | export default ({ children, show }) => ( 27 | 28 | {show ? ( 29 | 30 | 31 |
{children}
32 |
33 |
34 | ) : null} 35 |
36 | ); 37 | -------------------------------------------------------------------------------- /src/components/FullScreenThree.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./FullScreenOne.css"; 3 | import "./FullScreenTwo.css"; 4 | 5 | import { Dialog } from "@reach/dialog"; 6 | import "@reach/dialog/styles.css"; 7 | 8 | import { ScrollLocky } from "react-scroll-locky"; 9 | 10 | export default ({ children }) => ( 11 | 12 | {children} 13 | 14 | ); 15 | -------------------------------------------------------------------------------- /src/components/FullScreenTwo.css: -------------------------------------------------------------------------------- 1 | /* div to increase specificity */ 2 | div.FullScreenTwo{ 3 | width: 100%; 4 | height: 100%; 5 | margin: 0; 6 | padding: 0; 7 | } 8 | -------------------------------------------------------------------------------- /src/components/FullScreenTwo.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./FullScreenOne.css"; 3 | import "./FullScreenTwo.css"; 4 | 5 | import { Dialog } from "@reach/dialog"; 6 | import "@reach/dialog/styles.css"; 7 | 8 | export default ({ children }) => ( 9 | {children} 10 | ); 11 | -------------------------------------------------------------------------------- /src/components/fruits.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { value: "açaí" }, 3 | { value: "apple" }, 4 | { value: "akee" }, 5 | { value: "apricot" }, 6 | { value: "avocado" }, 7 | { value: "banana" }, 8 | { value: "bilberry" }, 9 | { value: "blackberry" }, 10 | { value: "blackcurrant" }, 11 | { value: "black sapote" }, 12 | { value: "blueberry" }, 13 | { value: "boysenberry" }, 14 | { value: "buddha's hand (fingered citron)" }, 15 | { value: "crab apples" }, 16 | { value: "currant" }, 17 | { value: "cherry" }, 18 | { value: "cherimoya (custard apple)" }, 19 | { value: "chico fruit" }, 20 | { value: "cloudberry" }, 21 | { value: "coconut" }, 22 | { value: "cranberry" }, 23 | { value: "cucumber" }, 24 | { value: "damson" }, 25 | { value: "date" }, 26 | { value: "dragonfruit (or pitaya)" }, 27 | { value: "durian" }, 28 | { value: "elderberry" }, 29 | { value: "feijoa" }, 30 | { value: "fig" }, 31 | { value: "goji berry" }, 32 | { value: "gooseberry" }, 33 | { value: "grape" }, 34 | { value: "raisin" }, 35 | { value: "grapefruit" }, 36 | { value: "guava" }, 37 | { value: "honeyberry" }, 38 | { value: "huckleberry" }, 39 | { value: "jabuticaba" }, 40 | { value: "jackfruit" }, 41 | { value: "jambul" }, 42 | { value: "japanese plum" }, 43 | { value: "jostaberry" }, 44 | { value: "jujube" }, 45 | { value: "juniper berry" }, 46 | { value: "kiwano (horned melon)" }, 47 | { value: "kiwifruit" }, 48 | { value: "kumquat" }, 49 | { value: "lemon" }, 50 | { value: "lime" }, 51 | { value: "loquat" }, 52 | { value: "longan" }, 53 | { value: "lychee" }, 54 | { value: "mango" }, 55 | { value: "mangosteen" }, 56 | { value: "marionberry" }, 57 | { value: "melon" }, 58 | { value: "cantaloupe" }, 59 | { value: "honeydew" }, 60 | { value: "watermelon" }, 61 | { value: "miracle fruit" }, 62 | { value: "mulberry" }, 63 | { value: "nectarine" }, 64 | { value: "nance" }, 65 | { value: "olive" }, 66 | { value: "orange" }, 67 | { value: "blood orange" }, 68 | { value: "clementine" }, 69 | { value: "mandarine" }, 70 | { value: "tangerine" }, 71 | { value: "papaya" }, 72 | { value: "passionfruit" }, 73 | { value: "peach" }, 74 | { value: "pear" }, 75 | { value: "persimmon" }, 76 | { value: "plantain" }, 77 | { value: "plum" }, 78 | { value: "prune (dried plum)" }, 79 | { value: "pineapple" }, 80 | { value: "pineberry" }, 81 | { value: "plumcot (or pluot)" }, 82 | { value: "pomegranate" }, 83 | { value: "pomelo" }, 84 | { value: "purple mangosteen" }, 85 | { value: "quince" }, 86 | { value: "raspberry" }, 87 | { value: "salmonberry" }, 88 | { value: "rambutan (or mamin chino)" }, 89 | { value: "redcurrant" }, 90 | { value: "salal berry" }, 91 | { value: "salak" }, 92 | { value: "satsuma" }, 93 | { value: "soursop" }, 94 | { value: "star apple" }, 95 | { value: "star fruit" }, 96 | { value: "strawberry" }, 97 | { value: "surinam cherry" }, 98 | { value: "tamarillo" }, 99 | { value: "tamarind" }, 100 | { value: "ugli fruit" }, 101 | { value: "yuzu" }, 102 | { value: "white currant" }, 103 | { value: "white sapote" } 104 | ]; 105 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { hydrate, render } from "react-dom"; 3 | import "normalize.css/normalize.css"; 4 | import App from "./App"; 5 | // import registerServiceWorker from './registerServiceWorker'; 6 | 7 | const rootElement = document.getElementById("root"); 8 | if (rootElement.hasChildNodes()) { 9 | hydrate(, rootElement); 10 | } else { 11 | render(, rootElement); 12 | } 13 | // registerServiceWorker(); 14 | -------------------------------------------------------------------------------- /src/registerServiceWorker.js: -------------------------------------------------------------------------------- 1 | // In production, we register a service worker to serve assets from local cache. 2 | 3 | // This lets the app load faster on subsequent visits in production, and gives 4 | // it offline capabilities. However, it also means that developers (and users) 5 | // will only see deployed updates on the "N+1" visit to a page, since previously 6 | // cached resources are updated in the background. 7 | 8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy. 9 | // This link also includes instructions on opting out of this behavior. 10 | 11 | const isLocalhost = Boolean( 12 | window.location.hostname === 'localhost' || 13 | // [::1] is the IPv6 localhost address. 14 | window.location.hostname === '[::1]' || 15 | // 127.0.0.1/8 is considered localhost for IPv4. 16 | window.location.hostname.match( 17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 18 | ) 19 | ); 20 | 21 | export default function register() { 22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 23 | // The URL constructor is available in all browsers that support SW. 24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location); 25 | if (publicUrl.origin !== window.location.origin) { 26 | // Our service worker won't work if PUBLIC_URL is on a different origin 27 | // from what our page is served on. This might happen if a CDN is used to 28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374 29 | return; 30 | } 31 | 32 | window.addEventListener('load', () => { 33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 34 | 35 | if (isLocalhost) { 36 | // This is running on localhost. Lets check if a service worker still exists or not. 37 | checkValidServiceWorker(swUrl); 38 | 39 | // Add some additional logging to localhost, pointing developers to the 40 | // service worker/PWA documentation. 41 | navigator.serviceWorker.ready.then(() => { 42 | console.log( 43 | 'This web app is being served cache-first by a service ' + 44 | 'worker. To learn more, visit https://goo.gl/SC7cgQ' 45 | ); 46 | }); 47 | } else { 48 | // Is not local host. Just register service worker 49 | registerValidSW(swUrl); 50 | } 51 | }); 52 | } 53 | } 54 | 55 | function registerValidSW(swUrl) { 56 | navigator.serviceWorker 57 | .register(swUrl) 58 | .then(registration => { 59 | registration.onupdatefound = () => { 60 | const installingWorker = registration.installing; 61 | installingWorker.onstatechange = () => { 62 | if (installingWorker.state === 'installed') { 63 | if (navigator.serviceWorker.controller) { 64 | // At this point, the old content will have been purged and 65 | // the fresh content will have been added to the cache. 66 | // It's the perfect time to display a "New content is 67 | // available; please refresh." message in your web app. 68 | console.log('New content is available; please refresh.'); 69 | } else { 70 | // At this point, everything has been precached. 71 | // It's the perfect time to display a 72 | // "Content is cached for offline use." message. 73 | console.log('Content is cached for offline use.'); 74 | } 75 | } 76 | }; 77 | }; 78 | }) 79 | .catch(error => { 80 | console.error('Error during service worker registration:', error); 81 | }); 82 | } 83 | 84 | function checkValidServiceWorker(swUrl) { 85 | // Check if the service worker can be found. If it can't reload the page. 86 | fetch(swUrl) 87 | .then(response => { 88 | // Ensure service worker exists, and that we really are getting a JS file. 89 | if ( 90 | response.status === 404 || 91 | response.headers.get('content-type').indexOf('javascript') === -1 92 | ) { 93 | // No service worker found. Probably a different app. Reload the page. 94 | navigator.serviceWorker.ready.then(registration => { 95 | registration.unregister().then(() => { 96 | window.location.reload(); 97 | }); 98 | }); 99 | } else { 100 | // Service worker found. Proceed as normal. 101 | registerValidSW(swUrl); 102 | } 103 | }) 104 | .catch(() => { 105 | console.log( 106 | 'No internet connection found. App is running in offline mode.' 107 | ); 108 | }); 109 | } 110 | 111 | export function unregister() { 112 | if ('serviceWorker' in navigator) { 113 | navigator.serviceWorker.ready.then(registration => { 114 | registration.unregister(); 115 | }); 116 | } 117 | } 118 | --------------------------------------------------------------------------------