Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(query-on-demand): loading indicator #28562

Merged
merged 13 commits into from
Dec 10, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions packages/gatsby/cache-dir/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import socketIo from "./socketIo"
import emitter from "./emitter"
import { apiRunner, apiRunnerAsync } from "./api-runner-browser"
import { setLoader, publicLoader } from "./loader"
import { Indicator } from "./loading-indicator/indicator"
import DevLoader from "./dev-loader"
import syncRequires from "$virtual/sync-requires"
// Generated during bootstrap
Expand Down Expand Up @@ -122,6 +123,30 @@ apiRunnerAsync(`onClientEntry`).then(() => {
ReactDOM.render
)[0]

let dismissLoadingIndicator
if (
process.env.GATSBY_EXPERIMENTAL_QUERY_ON_DEMAND &&
process.env.GATSBY_QUERY_ON_DEMAND_LOADING_INDICATOR === `true`
) {
let indicatorMountElement

const showIndicatorTimeout = setTimeout(() => {
indicatorMountElement = document.createElement(
`first-render-loading-indicator`
)
document.body.append(indicatorMountElement)
ReactDOM.render(<Indicator />, indicatorMountElement)
}, 1000)

dismissLoadingIndicator = () => {
clearTimeout(showIndicatorTimeout)
if (indicatorMountElement) {
ReactDOM.unmountComponentAtNode(indicatorMountElement)
indicatorMountElement.remove()
}
}
}

Promise.all([
loader.loadPage(`/dev-404-page/`),
loader.loadPage(`/404.html`),
Expand All @@ -130,6 +155,10 @@ apiRunnerAsync(`onClientEntry`).then(() => {
const preferDefault = m => (m && m.default) || m
const Root = preferDefault(require(`./root`))
domReady(() => {
if (dismissLoadingIndicator) {
dismissLoadingIndicator()
}

renderer(<Root />, rootElement, () => {
apiRunner(`onInitialClientRender`)
})
Expand Down
13 changes: 13 additions & 0 deletions packages/gatsby/cache-dir/debug-log.js
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)
}
31 changes: 31 additions & 0 deletions packages/gatsby/cache-dir/loading-indicator/index.js
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} />
}
}
64 changes: 64 additions & 0 deletions packages/gatsby/cache-dir/loading-indicator/indicator.js
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`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should also add a link to query on demand umrella here? @pragmaticpat @pieh

)
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>
)
}
43 changes: 43 additions & 0 deletions packages/gatsby/cache-dir/loading-indicator/portal.js
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
115 changes: 115 additions & 0 deletions packages/gatsby/cache-dir/loading-indicator/style.js
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
6 changes: 6 additions & 0 deletions packages/gatsby/cache-dir/navigation.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ const onPreRouteUpdate = (location, prevLocation) => {
const onRouteUpdate = (location, prevLocation) => {
if (!maybeRedirect(location.pathname)) {
apiRunner(`onRouteUpdate`, { location, prevLocation })
if (
process.env.GATSBY_EXPERIMENTAL_QUERY_ON_DEMAND &&
process.env.GATSBY_QUERY_ON_DEMAND_LOADING_INDICATOR === `true`
) {
emitter.emit(`onRouteUpdate`, { location, prevLocation })
}
}
}

Expand Down
5 changes: 5 additions & 0 deletions packages/gatsby/cache-dir/root.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import EnsureResources from "./ensure-resources"
import FastRefreshOverlay from "./fast-refresh-overlay"

import { reportError, clearError } from "./error-overlay-handler"
import { LoadingIndicatorEventHandler } from "./loading-indicator"

// TODO: Remove entire block when we make fast-refresh the default
// In fast-refresh, this logic is all moved into the `error-overlay-handler`
Expand Down Expand Up @@ -147,5 +148,9 @@ const ConditionalFastRefreshOverlay = ({ children }) => {
export default () => (
<ConditionalFastRefreshOverlay>
<StaticQueryStore>{WrappedRoot}</StaticQueryStore>
{process.env.GATSBY_EXPERIMENTAL_QUERY_ON_DEMAND &&
process.env.GATSBY_QUERY_ON_DEMAND_LOADING_INDICATOR === `true` && (
<LoadingIndicatorEventHandler />
)}
</ConditionalFastRefreshOverlay>
)
12 changes: 12 additions & 0 deletions packages/gatsby/src/services/initialize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,18 @@ export async function initialize({

process.env.GATSBY_HOT_LOADER = getReactHotLoaderStrategy()

// TODO: figure out proper way of disabling loading indicator
// for now GATSBY_QUERY_ON_DEMAND_LOADING_INDICATOR=false gatsby develop
// will work, but we don't want to force users into using env vars
if (
process.env.GATSBY_EXPERIMENTAL_QUERY_ON_DEMAND &&
!process.env.GATSBY_QUERY_ON_DEMAND_LOADING_INDICATOR
) {
// if query on demand is enabled and GATSBY_QUERY_ON_DEMAND_LOADING_INDICATOR was not set at all
// enable loading indicator
process.env.GATSBY_QUERY_ON_DEMAND_LOADING_INDICATOR = `true`
}

// theme gatsby configs can be functions or objects
if (config && config.__experimentalThemes) {
reporter.warn(
Expand Down
Loading