Skip to content

Commit

Permalink
feat(qwik-city): scroll restoration any element (#6258)
Browse files Browse the repository at this point in the history
Enable to change the scroller element to other than the `documentElement`
  • Loading branch information
genki committed May 7, 2024
1 parent 95eaef4 commit 4771896
Show file tree
Hide file tree
Showing 8 changed files with 47 additions and 16 deletions.
14 changes: 14 additions & 0 deletions packages/docs/src/routes/api/qwik-city/api.json
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,20 @@
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik-city/runtime/src/types.ts",
"mdFile": "qwik-city.pathparams.md"
},
{
"name": "QWIK_CITY_SCROLLER",
"id": "qwik_city_scroller",
"hierarchy": [
{
"name": "QWIK_CITY_SCROLLER",
"id": "qwik_city_scroller"
}
],
"kind": "Variable",
"content": "```typescript\nQWIK_CITY_SCROLLER = \"_qCityScroller\"\n```",
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik-city/runtime/src/qwik-city-component.tsx",
"mdFile": "qwik-city.qwik_city_scroller.md"
},
{
"name": "QwikCityMockProps",
"id": "qwikcitymockprops",
Expand Down
8 changes: 8 additions & 0 deletions packages/docs/src/routes/api/qwik-city/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -1681,6 +1681,14 @@ export declare type PathParams = Record<string, string>;
[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik-city/runtime/src/types.ts)
## QWIK_CITY_SCROLLER
```typescript
QWIK_CITY_SCROLLER = "_qCityScroller";
```
[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik-city/runtime/src/qwik-city-component.tsx)
## QwikCityMockProps
```typescript
Expand Down
4 changes: 2 additions & 2 deletions packages/docs/src/routes/api/qwik/api.json
Original file line number Diff line number Diff line change
Expand Up @@ -1704,7 +1704,7 @@
}
],
"kind": "Function",
"content": "> This API is provided as an alpha preview for developers and may change based on feedback that we receive. Do not use this API in a production environment.\n> \n\nLoad the prefetch graph for the container.\n\nEach Qwik container needs to include its own prefetch graph.\n\n\n```typescript\nPrefetchGraph: (opts?: {\n base?: string;\n manifestHash?: string;\n manifestURL?: string;\n}) => import(\"@builder.io/qwik/jsx-runtime\").JSXNode<\"script\">\n```\n\n\n<table><thead><tr><th>\n\nParameter\n\n\n</th><th>\n\nType\n\n\n</th><th>\n\nDescription\n\n\n</th></tr></thead>\n<tbody><tr><td>\n\nopts\n\n\n</td><td>\n\n{ base?: string; manifestHash?: string; manifestURL?: string; }\n\n\n</td><td>\n\n_(Optional)_ Options for the loading prefetch graph.\n\n- `base` - Base of the graph. For a default installation this will default to `/build/`<!-- -->. But if more than one MFE is installed on the page, then each MFE needs to have its own base. - `manifestHash` - Hash of the manifest file to load. If not provided the hash will be extracted from the container attribute `q:manifest-hash` and assume the default build file `${base}/q-bundle-graph-${manifestHash}.json`<!-- -->. - `manifestURL` - URL of the manifest file to load if non-standard bundle graph location name.\n\n\n</td></tr>\n</tbody></table>\n**Returns:**\n\nimport(\"@builder.io/qwik/jsx-runtime\").[JSXNode](#jsxnode)<!-- -->&lt;\"script\"&gt;",
"content": "> This API is provided as an alpha preview for developers and may change based on feedback that we receive. Do not use this API in a production environment.\n> \n\nLoad the prefetch graph for the container.\n\nEach Qwik container needs to include its own prefetch graph.\n\n\n```typescript\nPrefetchGraph: (opts?: {\n base?: string;\n manifestHash?: string;\n manifestURL?: string;\n}) => import(\"@builder.io/qwik/jsx-runtime\").JSXNode<\"script\">\n```\n\n\n<table><thead><tr><th>\n\nParameter\n\n\n</th><th>\n\nType\n\n\n</th><th>\n\nDescription\n\n\n</th></tr></thead>\n<tbody><tr><td>\n\nopts\n\n\n</td><td>\n\n{ base?: string; manifestHash?: string; manifestURL?: string; }\n\n\n</td><td>\n\n_(Optional)_ Options for the loading prefetch graph.\n\n- `base` - Base of the graph. For a default installation this will default to `/build/`<!-- -->. But if more than one MFE is installed on the page, then each MFE needs to have its own base. - `manifestHash` - Hash of the manifest file to load. If not provided the hash will be extracted from the container attribute `q:manifest-hash` and assume the default build file `${base}/q-bundle-graph-${manifestHash}.json`<!-- -->. - `manifestURL` - URL of the manifest file to load if non-standard bundle graph location name.\n\n\n</td></tr>\n</tbody></table>\n**Returns:**\n\nimport(\"@builder.io/qwik/jsx-runtime\").JSXNode&lt;\"script\"&gt;",
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/components/prefetch.ts",
"mdFile": "qwik.prefetchgraph.md"
},
Expand All @@ -1718,7 +1718,7 @@
}
],
"kind": "Function",
"content": "> This API is provided as an alpha preview for developers and may change based on feedback that we receive. Do not use this API in a production environment.\n> \n\nInstall a service worker which will prefetch the bundles.\n\nThere can only be one service worker per page. Because there can be many separate Qwik Containers on the page each container needs to load its prefetch graph using `PrefetchGraph` component.\n\n\n```typescript\nPrefetchServiceWorker: (opts: {\n base?: string;\n path?: string;\n verbose?: boolean;\n fetchBundleGraph?: boolean;\n}) => import(\"@builder.io/qwik/jsx-runtime\").JSXNode<\"script\">\n```\n\n\n<table><thead><tr><th>\n\nParameter\n\n\n</th><th>\n\nType\n\n\n</th><th>\n\nDescription\n\n\n</th></tr></thead>\n<tbody><tr><td>\n\nopts\n\n\n</td><td>\n\n{ base?: string; path?: string; verbose?: boolean; fetchBundleGraph?: boolean; }\n\n\n</td><td>\n\nOptions for the prefetch service worker.\n\n- `base` - Base URL for the service worker. - `path` - Path to the service worker.\n\n\n</td></tr>\n</tbody></table>\n**Returns:**\n\nimport(\"@builder.io/qwik/jsx-runtime\").[JSXNode](#jsxnode)<!-- -->&lt;\"script\"&gt;",
"content": "> This API is provided as an alpha preview for developers and may change based on feedback that we receive. Do not use this API in a production environment.\n> \n\nInstall a service worker which will prefetch the bundles.\n\nThere can only be one service worker per page. Because there can be many separate Qwik Containers on the page each container needs to load its prefetch graph using `PrefetchGraph` component.\n\n\n```typescript\nPrefetchServiceWorker: (opts: {\n base?: string;\n path?: string;\n verbose?: boolean;\n fetchBundleGraph?: boolean;\n}) => import(\"@builder.io/qwik/jsx-runtime\").JSXNode<\"script\">\n```\n\n\n<table><thead><tr><th>\n\nParameter\n\n\n</th><th>\n\nType\n\n\n</th><th>\n\nDescription\n\n\n</th></tr></thead>\n<tbody><tr><td>\n\nopts\n\n\n</td><td>\n\n{ base?: string; path?: string; verbose?: boolean; fetchBundleGraph?: boolean; }\n\n\n</td><td>\n\nOptions for the prefetch service worker.\n\n- `base` - Base URL for the service worker. - `path` - Path to the service worker.\n\n\n</td></tr>\n</tbody></table>\n**Returns:**\n\nimport(\"@builder.io/qwik/jsx-runtime\").JSXNode&lt;\"script\"&gt;",
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/components/prefetch.ts",
"mdFile": "qwik.prefetchserviceworker.md"
},
Expand Down
6 changes: 3 additions & 3 deletions packages/docs/src/routes/api/qwik/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -1515,9 +1515,9 @@ A unique ID for the context.
Low-level API for platform abstraction.
Different platforms (browser, node, service workers) may have different ways of handling things such as `requestAnimationFrame` and imports. To make Qwik platform independent, the `CorePlatform` API is used to access platform specific APIs.
Different platforms (browser, node, service workers) may have different ways of handling things such as `requestAnimationFrame` and imports. To make Qwik platform-independent Qwik uses the `CorePlatform` API to access the platform API.
`CorePlatform` is also responsible for importing symbols. Because the import map is different on the client (browser) than on the server, the server uses a manifest to map symbols to javascript chunks. Since this manifest is encapsulated in `CorePlatform`, `CorePlatform` cannot be global as there may be multiple applications running on the server concurrently.
`CorePlatform` also is responsible for importing symbols. The import map is different on the client (browser) then on the server. For this reason, the server has a manifest that is used to map symbols to javascript chunks. The manifest is encapsulated in `CorePlatform`, for this reason, the `CorePlatform` can't be global as there may be multiple applications running at server concurrently.
This is a low-level API and there should not be a need for you to access this.
Expand Down Expand Up @@ -1694,7 +1694,7 @@ Create a context ID to be used in your application. The name should be written w
Context is a way to pass stores to the child components without prop-drilling.
Use `createContextId()` to create a `ContextId`. A `ContextId` is just a serializable identifier for the context. It is not the context value itself. See `useContextProvider()` and `useContext()` for the values. Qwik needs a serializable ID for the context so that it can track context providers and consumers in a way that survives resumability.
Use `createContextId()` to create a `ContextId`. A `ContextId` is just a serializable identifier for the context. It is not the context value itself. See `useContextProvider()` and `useContext()` for the values. Qwik needs a serializable ID for the context so that the it can track context providers and consumers in a way that survives resumability.
### Example
Expand Down
3 changes: 3 additions & 0 deletions packages/qwik-city/runtime/src/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,9 @@ export interface PageModule extends RouteModule {
// @public (undocumented)
export type PathParams = Record<string, string>;

// @public (undocumented)
export const QWIK_CITY_SCROLLER = "_qCityScroller";

// @public (undocumented)
export interface QwikCityMockProps {
// (undocumented)
Expand Down
1 change: 1 addition & 0 deletions packages/qwik-city/runtime/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export {
QwikCityProvider,
type QwikCityMockProps,
QwikCityMockProvider,
QWIK_CITY_SCROLLER,
} from './qwik-city-component';
export { type LinkProps, Link } from './link-component';
export { ServiceWorkerRegister } from './sw-component';
Expand Down
22 changes: 13 additions & 9 deletions packages/qwik-city/runtime/src/qwik-city-component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ import {
} from './scroll-restoration';
import spaInit from './spa-init';

/** @public */
export const QWIK_CITY_SCROLLER = '_qCityScroller';

/** @public */
export interface QwikCityProps {
// /**
Expand Down Expand Up @@ -168,7 +171,8 @@ export const QwikCityProvider = component$<QwikCityProps>((props) => {
}

// Always scroll on same-page popstates, #hash clicks, or links.
restoreScroll(type, dest, new URL(location.href), getScrollHistory());
const scroller = document.getElementById(QWIK_CITY_SCROLLER) ?? document.documentElement;
restoreScroll(type, dest, new URL(location.href), scroller, getScrollHistory());

if (type === 'popstate') {
(window as ClientSPAWindow)._qCityScrollEnabled = true;
Expand Down Expand Up @@ -309,6 +313,7 @@ export const QwikCityProvider = component$<QwikCityProps>((props) => {
if (navType === 'popstate') {
scrollState = getScrollHistory();
}
const scroller = document.getElementById(QWIK_CITY_SCROLLER) ?? document.documentElement;

if (
(navigation.scroll &&
Expand All @@ -319,7 +324,7 @@ export const QwikCityProvider = component$<QwikCityProps>((props) => {
) {
// Mark next DOM render to scroll.
(document as any).__q_scroll_restore__ = () =>
restoreScroll(navType, trackUrl, prevUrl, scrollState);
restoreScroll(navType, trackUrl, prevUrl, scroller, scrollState);
}

const loaders = clientPageData?.loaders;
Expand Down Expand Up @@ -373,8 +378,7 @@ export const QwikCityProvider = component$<QwikCityProps>((props) => {
}
}

state._qCityScroll =
state._qCityScroll || currentScrollState(document.documentElement);
state._qCityScroll = state._qCityScroll || currentScrollState(scroller);
return state;
};

Expand Down Expand Up @@ -419,7 +423,7 @@ export const QwikCityProvider = component$<QwikCityProps>((props) => {
win._qCityScrollEnabled = false;
clearTimeout(win._qCityScrollDebounce);
saveScrollHistory({
...currentScrollState(document.documentElement),
...currentScrollState(scroller),
x: 0,
y: 0,
});
Expand All @@ -446,7 +450,7 @@ export const QwikCityProvider = component$<QwikCityProps>((props) => {
if (win._qCityScrollEnabled && document.visibilityState === 'hidden') {
// Last & most reliable point to commit state.
// Do not clear timeout here in case debounce gets to run later.
const scrollState = currentScrollState(document.documentElement);
const scrollState = currentScrollState(scroller);
saveScrollHistory(scrollState);
}
},
Expand All @@ -466,7 +470,7 @@ export const QwikCityProvider = component$<QwikCityProps>((props) => {

clearTimeout(win._qCityScrollDebounce);
win._qCityScrollDebounce = setTimeout(() => {
const scrollState = currentScrollState(document.documentElement);
const scrollState = currentScrollState(scroller);
saveScrollHistory(scrollState);
// Needed for e2e debounceDetector.
win._qCityScrollDebounce = undefined;
Expand All @@ -491,15 +495,15 @@ export const QwikCityProvider = component$<QwikCityProps>((props) => {

// Save the final scroll state before pushing new state.
// Upgrades/replaces state with scroll pos on nav as needed.
const scrollState = currentScrollState(document.documentElement);
const scrollState = currentScrollState(scroller);
saveScrollHistory(scrollState);
}

clientNavigate(window, navType, prevUrl, trackUrl, replaceState);
_waitUntilRendered(elm as Element).then(() => {
const container = getContainer(elm as Element);
container.setAttribute('q:route', routeName);
const scrollState = currentScrollState(document.documentElement);
const scrollState = currentScrollState(scroller);
saveScrollHistory(scrollState);
win._qCityScrollEnabled = true;

Expand Down
5 changes: 3 additions & 2 deletions packages/qwik-city/runtime/src/scroll-restoration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@ export const restoreScroll = (
type: NavigationType,
toUrl: URL,
fromUrl: URL,
scroller: Element,
scrollState?: ScrollState
) => {
if (type === 'popstate' && scrollState) {
window.scrollTo(scrollState.x, scrollState.y);
scroller.scrollTo(scrollState.x, scrollState.y);
} else if (type === 'link' || type === 'form') {
if (!hashScroll(toUrl, fromUrl)) {
window.scrollTo(0, 0);
scroller.scrollTo(0, 0);
}
}
};
Expand Down

0 comments on commit 4771896

Please sign in to comment.