-
Notifications
You must be signed in to change notification settings - Fork 10.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(gatsby): loading indicator for query-on-demand (#28562)
* initial setup to show loading indicator when loading page resources takes over a second this will show both for client side navigation and first render * style loading indicator * add gatsby specifc debugLog function for runtime to show 'gatsby' prefixed debug logs * allow to enable/disable loading indicator via /___loading-indicator/disable get request * print console message on first render of indicator how to disable it * fix non query on demand bundling, don't even add loading components if loading indicator is not enabled, disable by default in cypress env * fix /___loading-indicator/disable check * add instructions about disabling via flag config * announce loading indicator + add media query for reduced motion and dark theme * don't announce out * cleanup DOM after first-render loading indicator * drop config flag support - this need more thinking because it would set precedent that would add configuration options to flags and we might regret it later * re-word browser console message, thanks @pragmaticpat Co-authored-by: LekoArts <[email protected]>
- Loading branch information
Showing
11 changed files
with
385 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
// inspired by https://github.com/GoogleChrome/workbox/blob/3d02230f0e977eb1dc86c48f16ea4bcefdae12af/packages/workbox-core/src/_private/logger.ts | ||
|
||
const styles = [ | ||
`background: rebeccapurple`, | ||
`border-radius: 0.5em`, | ||
`color: white`, | ||
`font-weight: bold`, | ||
`padding: 2px 0.5em`, | ||
].join(`;`) | ||
|
||
export function debugLog(...args) { | ||
console.debug(`%cgatsby`, styles, ...args) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import React from "react" | ||
|
||
import emitter from "../emitter" | ||
import { Indicator } from "./indicator" | ||
|
||
// no hooks because we support react versions without hooks support | ||
export class LoadingIndicatorEventHandler extends React.Component { | ||
state = { visible: false } | ||
|
||
show = () => { | ||
this.setState({ visible: true }) | ||
} | ||
|
||
hide = () => { | ||
this.setState({ visible: false }) | ||
} | ||
|
||
componentDidMount() { | ||
emitter.on(`onDelayedLoadPageResources`, this.show) | ||
emitter.on(`onRouteUpdate`, this.hide) | ||
} | ||
|
||
componentWillUnmount() { | ||
emitter.off(`onDelayedLoadPageResources`, this.show) | ||
emitter.off(`onRouteUpdate`, this.hide) | ||
} | ||
|
||
render() { | ||
return <Indicator visible={this.state.visible} /> | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
import React from "react" | ||
import Portal from "./portal" | ||
import Style from "./style" | ||
import { isLoadingIndicatorEnabled } from "$virtual/loading-indicator" | ||
import { debugLog } from "../debug-log" | ||
|
||
if (typeof window === `undefined`) { | ||
throw new Error( | ||
`Loading indicator should never be imported in code that doesn't target only browsers` | ||
) | ||
} | ||
|
||
if (module.hot) { | ||
module.hot.accept(`$virtual/loading-indicator`, () => { | ||
// isLoadingIndicatorEnabled is imported with ES import so no need | ||
// for dedicated handling as HMR just replace it in that case | ||
}) | ||
} | ||
|
||
// HMR can rerun this, so check if it was set before | ||
// we also set it on window and not just in module scope because of HMR resetting | ||
// module scope | ||
if (typeof window.___gatsbyDidShowLoadingIndicatorBefore === `undefined`) { | ||
window.___gatsbyDidShowLoadingIndicatorBefore = false | ||
} | ||
|
||
export function Indicator({ visible = true }) { | ||
if (!isLoadingIndicatorEnabled()) { | ||
return null | ||
} | ||
|
||
if (!window.___gatsbyDidShowLoadingIndicatorBefore) { | ||
// not ideal to this in render function, but that's just console info | ||
debugLog( | ||
`A loading indicator is displayed in-browser whenever content is being requested upon navigation (Query On Demand).\n\nYou can disable the loading indicator for your current session by visiting ${window.location.origin}/___loading-indicator/disable` | ||
) | ||
window.___gatsbyDidShowLoadingIndicatorBefore = true | ||
} | ||
|
||
return ( | ||
<Portal> | ||
<Style /> | ||
<div | ||
data-gatsby-loading-indicator="root" | ||
data-gatsby-loading-indicator-visible={visible} | ||
aria-live="assertive" | ||
> | ||
<div data-gatsby-loading-indicator="spinner" aria-hidden="true"> | ||
<svg | ||
xmlns="http://www.w3.org/2000/svg" | ||
viewBox="0 0 24 24" | ||
fill="currentColor" | ||
> | ||
<path d="M0 0h24v24H0z" fill="none" /> | ||
<path d="M12 6v3l4-4-4-4v3c-4.42 0-8 3.58-8 8 0 1.57.46 3.03 1.24 4.26L6.7 14.8c-.45-.83-.7-1.79-.7-2.8 0-3.31 2.69-6 6-6zm6.76 1.74L17.3 9.2c.44.84.7 1.79.7 2.8 0 3.31-2.69 6-6 6v-3l-4 4 4 4v-3c4.42 0 8-3.58 8-8 0-1.57-.46-3.03-1.24-4.26z" /> | ||
</svg> | ||
</div> | ||
<div data-gatsby-loading-indicator="text"> | ||
{visible ? `Preparing requested page` : ``} | ||
</div> | ||
</div> | ||
</Portal> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
import * as React from "react" | ||
import { createPortal } from "react-dom" | ||
|
||
// this is `fast-refresh-overlay/portal` ported to class component | ||
// because we don't have guarantee that query on demand users will use | ||
// react version that supports hooks | ||
// TO-DO: consolidate both portals into single shared component (need testing) | ||
class ShadowPortal extends React.Component { | ||
mountNode = React.createRef(null) | ||
portalNode = React.createRef(null) | ||
shadowNode = React.createRef(null) | ||
state = { | ||
createdElement: false, | ||
} | ||
|
||
componentDidMount() { | ||
const ownerDocument = this.mountNode.current.ownerDocument | ||
this.portalNode.current = ownerDocument.createElement(`gatsby-portal`) | ||
this.shadowNode.current = this.portalNode.current.attachShadow({ | ||
mode: `open`, | ||
}) | ||
ownerDocument.body.appendChild(this.portalNode.current) | ||
this.setState({ createdElement: true }) | ||
} | ||
|
||
componentWillUnmount() { | ||
if (this.portalNode.current && this.portalNode.current.ownerDocument) { | ||
this.portalNode.current.ownerDocument.body.removeChild( | ||
this.portalNode.current | ||
) | ||
} | ||
} | ||
|
||
render() { | ||
return this.shadowNode.current ? ( | ||
createPortal(this.props.children, this.shadowNode.current) | ||
) : ( | ||
<span ref={this.mountNode} /> | ||
) | ||
} | ||
} | ||
|
||
export default ShadowPortal |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
import React from "react" | ||
|
||
function css(strings, ...keys) { | ||
const lastIndex = strings.length - 1 | ||
return ( | ||
strings.slice(0, lastIndex).reduce((p, s, i) => p + s + keys[i], ``) + | ||
strings[lastIndex] | ||
) | ||
} | ||
|
||
const Style = () => ( | ||
<style | ||
dangerouslySetInnerHTML={{ | ||
__html: css` | ||
:host { | ||
--purple-60: #663399; | ||
--gatsby: var(--purple-60); | ||
--purple-40: #b17acc; | ||
--purple-20: #f1defa; | ||
--dimmedWhite: rgba(255, 255, 255, 0.8); | ||
--white: #ffffff; | ||
--black: #000000; | ||
--grey-90: #232129; | ||
--radii: 4px; | ||
--z-index-normal: 5; | ||
--z-index-elevated: 10; | ||
--shadow: 0px 2px 4px rgba(46, 41, 51, 0.08), | ||
0px 4px 8px rgba(71, 63, 79, 0.16); | ||
} | ||
[data-gatsby-loading-indicator="root"] { | ||
font: 14px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, | ||
Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", | ||
"Segoe UI Symbol" !important; | ||
background: var(--white); | ||
color: var(--grey-90); | ||
position: fixed; | ||
bottom: 1.5em; | ||
left: 1.5em; | ||
box-shadow: var(--shadow); | ||
border-radius: var(--radii); | ||
z-index: var(--z-index-elevated); | ||
border-left: 0.25em solid var(--purple-40); | ||
display: flex; | ||
align-items: center; | ||
justify-content: space-between; | ||
flex-wrap: nowrap; | ||
padding: 0.75em 1.15em; | ||
min-width: 196px; | ||
} | ||
[data-gatsby-loading-indicator-visible="false"] { | ||
opacity: 0; | ||
visibility: hidden; | ||
will-change: opacity, transform; | ||
transform: translateY(45px); | ||
transition: all 0.3s ease-in-out; | ||
} | ||
[data-gatsby-loading-indicator-visible="true"] { | ||
opacity: 1; | ||
visibility: visible; | ||
transform: translateY(0px); | ||
transition: all 0.3s ease-in-out; | ||
} | ||
[data-gatsby-loading-indicator="spinner"] { | ||
animation: spin 1s linear infinite; | ||
height: 18px; | ||
width: 18px; | ||
color: var(--gatsby); | ||
} | ||
[data-gatsby-loading-indicator="text"] { | ||
margin-left: 0.75em; | ||
line-height: 18px; | ||
} | ||
@keyframes spin { | ||
0% { | ||
transform: rotate(0); | ||
} | ||
100% { | ||
transform: rotate(360deg); | ||
} | ||
} | ||
@media (prefers-reduced-motion: reduce) { | ||
[data-gatsby-loading-indicator="spinner"] { | ||
animation: none; | ||
} | ||
[data-gatsby-loading-indicator-visible="false"] { | ||
transition: none; | ||
} | ||
[data-gatsby-loading-indicator-visible="true"] { | ||
transition: none; | ||
} | ||
} | ||
@media (prefers-color-scheme: dark) { | ||
[data-gatsby-loading-indicator="root"] { | ||
background: var(--grey-90); | ||
color: var(--white); | ||
} | ||
[data-gatsby-loading-indicator="spinner"] { | ||
color: var(--purple-20); | ||
} | ||
} | ||
`, | ||
}} | ||
/> | ||
) | ||
|
||
export default Style |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.