diff --git a/packages/docs/src/routes/api/qwik-city/api.json b/packages/docs/src/routes/api/qwik-city/api.json index ecb800e83a4..084b18048c8 100644 --- a/packages/docs/src/routes/api/qwik-city/api.json +++ b/packages/docs/src/routes/api/qwik-city/api.json @@ -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", diff --git a/packages/docs/src/routes/api/qwik-city/index.md b/packages/docs/src/routes/api/qwik-city/index.md index 26644e608e5..7b82ecbfdae 100644 --- a/packages/docs/src/routes/api/qwik-city/index.md +++ b/packages/docs/src/routes/api/qwik-city/index.md @@ -1681,6 +1681,14 @@ export declare type PathParams = Record; [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 diff --git a/packages/docs/src/routes/api/qwik/api.json b/packages/docs/src/routes/api/qwik/api.json index 00a2e7799a9..dfb62646924 100644 --- a/packages/docs/src/routes/api/qwik/api.json +++ b/packages/docs/src/routes/api/qwik/api.json @@ -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\n\n
\n\nParameter\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
\n\nopts\n\n\n\n\n{ base?: string; manifestHash?: string; manifestURL?: string; }\n\n\n\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
\n**Returns:**\n\nimport(\"@builder.io/qwik/jsx-runtime\").[JSXNode](#jsxnode)<\"script\">", + "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\n\n
\n\nParameter\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
\n\nopts\n\n\n\n\n{ base?: string; manifestHash?: string; manifestURL?: string; }\n\n\n\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
\n**Returns:**\n\nimport(\"@builder.io/qwik/jsx-runtime\").JSXNode<\"script\">", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/components/prefetch.ts", "mdFile": "qwik.prefetchgraph.md" }, @@ -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\n\n
\n\nParameter\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
\n\nopts\n\n\n\n\n{ base?: string; path?: string; verbose?: boolean; fetchBundleGraph?: boolean; }\n\n\n\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
\n**Returns:**\n\nimport(\"@builder.io/qwik/jsx-runtime\").[JSXNode](#jsxnode)<\"script\">", + "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\n\n
\n\nParameter\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
\n\nopts\n\n\n\n\n{ base?: string; path?: string; verbose?: boolean; fetchBundleGraph?: boolean; }\n\n\n\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
\n**Returns:**\n\nimport(\"@builder.io/qwik/jsx-runtime\").JSXNode<\"script\">", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/components/prefetch.ts", "mdFile": "qwik.prefetchserviceworker.md" }, diff --git a/packages/docs/src/routes/api/qwik/index.md b/packages/docs/src/routes/api/qwik/index.md index 6c2567553f8..4b0efa2cdd8 100644 --- a/packages/docs/src/routes/api/qwik/index.md +++ b/packages/docs/src/routes/api/qwik/index.md @@ -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. @@ -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 diff --git a/packages/qwik-city/runtime/src/api.md b/packages/qwik-city/runtime/src/api.md index 7c727751205..9c85597a2b9 100644 --- a/packages/qwik-city/runtime/src/api.md +++ b/packages/qwik-city/runtime/src/api.md @@ -306,6 +306,9 @@ export interface PageModule extends RouteModule { // @public (undocumented) export type PathParams = Record; +// @public (undocumented) +export const QWIK_CITY_SCROLLER = "_qCityScroller"; + // @public (undocumented) export interface QwikCityMockProps { // (undocumented) diff --git a/packages/qwik-city/runtime/src/index.ts b/packages/qwik-city/runtime/src/index.ts index e53fa283529..4904a0c4d27 100644 --- a/packages/qwik-city/runtime/src/index.ts +++ b/packages/qwik-city/runtime/src/index.ts @@ -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'; diff --git a/packages/qwik-city/runtime/src/qwik-city-component.tsx b/packages/qwik-city/runtime/src/qwik-city-component.tsx index 9b66c85b8b9..a8fddf5c785 100644 --- a/packages/qwik-city/runtime/src/qwik-city-component.tsx +++ b/packages/qwik-city/runtime/src/qwik-city-component.tsx @@ -57,6 +57,9 @@ import { } from './scroll-restoration'; import spaInit from './spa-init'; +/** @public */ +export const QWIK_CITY_SCROLLER = '_qCityScroller'; + /** @public */ export interface QwikCityProps { // /** @@ -166,7 +169,8 @@ export const QwikCityProvider = component$((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; @@ -307,6 +311,7 @@ export const QwikCityProvider = component$((props) => { if (navType === 'popstate') { scrollState = getScrollHistory(); } + const scroller = document.getElementById(QWIK_CITY_SCROLLER) ?? document.documentElement; if ( (navigation.scroll && @@ -317,7 +322,7 @@ export const QwikCityProvider = component$((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; @@ -371,8 +376,7 @@ export const QwikCityProvider = component$((props) => { } } - state._qCityScroll = - state._qCityScroll || currentScrollState(document.documentElement); + state._qCityScroll = state._qCityScroll || currentScrollState(scroller); return state; }; @@ -417,7 +421,7 @@ export const QwikCityProvider = component$((props) => { win._qCityScrollEnabled = false; clearTimeout(win._qCityScrollDebounce); saveScrollHistory({ - ...currentScrollState(document.documentElement), + ...currentScrollState(scroller), x: 0, y: 0, }); @@ -444,7 +448,7 @@ export const QwikCityProvider = component$((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); } }, @@ -464,7 +468,7 @@ export const QwikCityProvider = component$((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; @@ -489,7 +493,7 @@ export const QwikCityProvider = component$((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); } @@ -497,7 +501,7 @@ export const QwikCityProvider = component$((props) => { _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; diff --git a/packages/qwik-city/runtime/src/scroll-restoration.ts b/packages/qwik-city/runtime/src/scroll-restoration.ts index 20f27393ef1..0313430017b 100644 --- a/packages/qwik-city/runtime/src/scroll-restoration.ts +++ b/packages/qwik-city/runtime/src/scroll-restoration.ts @@ -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); } } };