eidos-ui 3.8.0 → 3.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-QUSO4GFA.cjs → chunk-GZDT2AHJ.cjs} +18 -18
- package/dist/chunk-GZDT2AHJ.cjs.map +1 -0
- package/dist/{chunk-6LTZV6HN.js → chunk-IC2PIK3E.js} +16 -16
- package/dist/chunk-IC2PIK3E.js.map +1 -0
- package/dist/index.cjs +2 -2
- package/dist/index.js +1 -1
- package/dist/page-layout/index.cjs +2 -2
- package/dist/page-layout/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-6LTZV6HN.js.map +0 -1
- package/dist/chunk-QUSO4GFA.cjs.map +0 -1
|
@@ -21,22 +21,22 @@ var useScrollRestoration = (key, forwardTo) => {
|
|
|
21
21
|
_react.useEffect.call(void 0, () => {
|
|
22
22
|
forwarded.current = forwardTo;
|
|
23
23
|
}, [forwardTo]);
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
if (
|
|
28
|
-
else if (ref) ref.current = element;
|
|
29
|
-
}, []);
|
|
30
|
-
_react.useEffect.call(void 0, () => {
|
|
31
|
-
const element = node.current;
|
|
32
|
-
if (!element) return;
|
|
33
|
-
const record = () => {
|
|
34
|
-
const current = appliedKey.current;
|
|
35
|
-
if (current !== void 0) offsets.current.set(current, element.scrollTop);
|
|
36
|
-
};
|
|
37
|
-
element.addEventListener("scroll", record, { passive: true });
|
|
38
|
-
return () => element.removeEventListener("scroll", record);
|
|
24
|
+
const record = _react.useCallback.call(void 0, (event) => {
|
|
25
|
+
const current = appliedKey.current;
|
|
26
|
+
const element = event.currentTarget;
|
|
27
|
+
if (current !== void 0) offsets.current.set(current, element.scrollTop);
|
|
39
28
|
}, []);
|
|
29
|
+
const setNode = _react.useCallback.call(void 0,
|
|
30
|
+
(element) => {
|
|
31
|
+
_optionalChain([node, 'access', _ => _.current, 'optionalAccess', _2 => _2.removeEventListener, 'call', _3 => _3("scroll", record)]);
|
|
32
|
+
node.current = element;
|
|
33
|
+
_optionalChain([element, 'optionalAccess', _4 => _4.addEventListener, 'call', _5 => _5("scroll", record, { passive: true })]);
|
|
34
|
+
const ref = forwarded.current;
|
|
35
|
+
if (typeof ref === "function") ref(element);
|
|
36
|
+
else if (ref) ref.current = element;
|
|
37
|
+
},
|
|
38
|
+
[record]
|
|
39
|
+
);
|
|
40
40
|
_react.useLayoutEffect.call(void 0, () => {
|
|
41
41
|
const element = node.current;
|
|
42
42
|
if (!element || key === void 0 || appliedKey.current === key) return;
|
|
@@ -69,7 +69,7 @@ var PageLayout = ({
|
|
|
69
69
|
_chunkTNQ2ZZ7Wcjs.Toolbar,
|
|
70
70
|
{
|
|
71
71
|
...toolbar,
|
|
72
|
-
className: ["eidos-pagelayout__toolbar", _optionalChain([toolbar, 'optionalAccess',
|
|
72
|
+
className: ["eidos-pagelayout__toolbar", _optionalChain([toolbar, 'optionalAccess', _6 => _6.className])].filter(Boolean).join(" ")
|
|
73
73
|
}
|
|
74
74
|
),
|
|
75
75
|
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { className: "eidos-pagelayout__navigation", children: navigation && /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkF5IRMMZ2cjs.Navigation, { ...navigation }) }),
|
|
@@ -86,7 +86,7 @@ var PageLayout = ({
|
|
|
86
86
|
_chunkPTBBY4ZXcjs.Footer,
|
|
87
87
|
{
|
|
88
88
|
..._nullishCoalesce(footer, () => ( defaultFooter)),
|
|
89
|
-
className: ["eidos-pagelayout__footer", _optionalChain([footer, 'optionalAccess',
|
|
89
|
+
className: ["eidos-pagelayout__footer", _optionalChain([footer, 'optionalAccess', _7 => _7.className])].filter(Boolean).join(" ")
|
|
90
90
|
}
|
|
91
91
|
)
|
|
92
92
|
] })
|
|
@@ -96,4 +96,4 @@ var PageLayout = ({
|
|
|
96
96
|
|
|
97
97
|
|
|
98
98
|
exports.PageLayout = PageLayout;
|
|
99
|
-
//# sourceMappingURL=chunk-
|
|
99
|
+
//# sourceMappingURL=chunk-GZDT2AHJ.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["/home/runner/work/eidos-ui/eidos-ui/dist/chunk-GZDT2AHJ.cjs","../src/components/PageLayout/useScrollRestoration.ts","../src/components/PageLayout/PageLayout.component.tsx"],"names":[],"mappings":"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","file":"/home/runner/work/eidos-ui/eidos-ui/dist/chunk-GZDT2AHJ.cjs","sourcesContent":[null,"import { useCallback, useEffect, useLayoutEffect, useRef } from 'react';\n\n/**\n * Remembers the content region's scroll offset per key, and applies the\n * stored offset whenever the key changes.\n *\n * The key is supplied by the consumer rather than derived here, because only\n * they know what counts as a navigation - this library has no router opinion.\n * That also decides the semantics, exactly as React Router's `getKey` does:\n * a per-path key gives \"this path always reopens where you left it\", while a\n * per-history-entry key gives the browser's own behaviour, where Back\n * restores but a fresh link to the same URL starts at the top.\n *\n * Returns a ref callback for the scroll container, which also forwards the\n * node to the caller's own `contentRef`.\n */\nexport const useScrollRestoration = (\n key: string | number | undefined,\n forwardTo: React.Ref<HTMLElement> | undefined,\n): ((node: HTMLElement | null) => void) => {\n const node = useRef<HTMLElement | null>(null);\n const offsets = useRef(new Map<string | number, number>());\n const appliedKey = useRef(key);\n\n // Held in a ref so the ref callback below can keep a stable identity. A\n // callback ref that changes identity is invoked with `null` and then the\n // node again on every render that changes it, which would detach and\n // reattach the caller's ref for no reason.\n const forwarded = useRef(forwardTo);\n useEffect(() => {\n forwarded.current = forwardTo;\n }, [forwardTo]);\n\n // Recorded as it happens rather than when the key changes: by the time a\n // navigation commits, the new page's content is already in the DOM and the\n // browser has clamped `scrollTop` to the new, possibly shorter, height. The\n // outgoing offset is no longer readable at that point.\n //\n // **The key is read from a ref, and the listener is registered once per\n // node.** Both halves matter. A listener re-subscribed per key closes over\n // the key it was created with, and restoring the incoming page's offset sets\n // `scrollTop`, which makes the browser dispatch a `scroll` event - so the\n // two race:\n //\n // 1. the layout effect below sets `scrollTop` for the incoming key\n // 2. passive effects swap the listener from the outgoing key to it\n // 3. the browser dispatches the scroll event from step 1\n //\n // React flushes passive effects through the scheduler and the browser\n // dispatches `scroll` at its own rendering opportunity, so nothing orders 2\n // before 3. When 3 won, the still-attached *outgoing* listener recorded the\n // incoming page's offset under the outgoing page's key - overwriting a real\n // offset with 0 and losing the position it exists to keep. It passed locally\n // and failed in CI, which is what a race of this shape looks like.\n //\n // Reading `appliedKey` instead means whichever order those steps take, the\n // offset is filed under the key that is actually on screen: the layout effect\n // updates the ref before it touches `scrollTop`.\n //\n // **Attached in the ref callback, not in an effect.** It used to be a\n // passive `useEffect`, which React runs some time *after* the commit - so a\n // scroll landing in between was never recorded, and returning to that page\n // restored 0. Storybook's production build starts a play function inside\n // that window (the dev server happens not to), which is how\n // `RestoresScrollPerKey` passed locally and failed on Chromatic, every time.\n // A ref callback runs during the commit itself, so the listener exists from\n // the moment the element does - and a swapped element is detached from and\n // re-attached to rather than silently left unobserved.\n const record = useCallback((event: Event) => {\n const current = appliedKey.current;\n const element = event.currentTarget as HTMLElement;\n if (current !== undefined) offsets.current.set(current, element.scrollTop);\n }, []);\n\n const setNode = useCallback(\n (element: HTMLElement | null) => {\n node.current?.removeEventListener('scroll', record);\n node.current = element;\n element?.addEventListener('scroll', record, { passive: true });\n\n const ref = forwarded.current;\n if (typeof ref === 'function') ref(element);\n else if (ref) (ref as React.RefObject<HTMLElement | null>).current = element;\n },\n [record],\n );\n\n // Layout effect so the offset is applied before paint - in a passive effect\n // the new page is visible at the previous page's offset for a frame first.\n // It does not run on the server, which is correct here: there is no scroll\n // position to restore in static markup, and the client applies it on mount.\n useLayoutEffect(() => {\n const element = node.current;\n if (!element || key === undefined || appliedKey.current === key) return;\n\n appliedKey.current = key;\n element.scrollTop = offsets.current.get(key) ?? 0;\n }, [key]);\n\n return setNode;\n};\n","import React from 'react';\nimport type { PageLayoutProps } from './PageLayout.types';\nimport './PageLayout.scss';\nimport { Toolbar } from '../Toolbar';\nimport { Header } from '../Header';\nimport { Footer } from '../Footer';\nimport type { FooterProps } from '../Footer';\nimport { Navigation } from '../Navigation';\nimport { useScrollRestoration } from './useScrollRestoration';\n\nconst defaultFooter: FooterProps = { copyright: `© ${new Date().getFullYear()} Eidos UI` };\n\nexport const PageLayout: React.FC<PageLayoutProps> = ({\n toolbar,\n navigation,\n header,\n footer,\n children,\n className = '',\n // Destructured so they cannot fall into `...rest` and land on the root\n // element, which is not the scroll container.\n contentRef,\n scrollRestorationKey,\n ...rest\n}) => {\n const classes = ['eidos-pagelayout', className].filter(Boolean).join(' ');\n const setContentRef = useScrollRestoration(scrollRestorationKey, contentRef);\n\n return (\n <div className={classes} {...rest}>\n {/* Spread rather than forwarded field by field, so a prop added to\n `Toolbar` reaches `PageLayout` consumers without a second edit\n here - the previous per-field list silently dropped anything it\n didn't know about. `className` is merged last so the layout's own\n grid-area class can't be overwritten by the caller's. */}\n <Toolbar\n {...toolbar}\n className={['eidos-pagelayout__toolbar', toolbar?.className].filter(Boolean).join(' ')}\n />\n <div className=\"eidos-pagelayout__navigation\">\n {navigation && <Navigation {...navigation} />}\n </div>\n {/* A scrollable region containing nothing focusable cannot be scrolled\n by keyboard at all, and a page body is often pure content - a\n report, an article. `tabIndex={0}` makes the region itself focusable\n so arrow keys and Page Up/Down reach it (SC 2.1.1), matching\n `VirtualList` and `Chat`. */}\n <main ref={setContentRef} tabIndex={0} className=\"eidos-pagelayout__content\">\n {header && (\n <Header\n {...header}\n className={['eidos-pagelayout__header', header.className].filter(Boolean).join(' ')}\n />\n )}\n <div className=\"eidos-pagelayout__body\">{children}</div>\n <Footer\n {...(footer ?? defaultFooter)}\n className={['eidos-pagelayout__footer', footer?.className].filter(Boolean).join(' ')}\n />\n </main>\n </div>\n );\n};\n"]}
|
|
@@ -21,22 +21,22 @@ var useScrollRestoration = (key, forwardTo) => {
|
|
|
21
21
|
useEffect(() => {
|
|
22
22
|
forwarded.current = forwardTo;
|
|
23
23
|
}, [forwardTo]);
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
if (
|
|
28
|
-
else if (ref) ref.current = element;
|
|
29
|
-
}, []);
|
|
30
|
-
useEffect(() => {
|
|
31
|
-
const element = node.current;
|
|
32
|
-
if (!element) return;
|
|
33
|
-
const record = () => {
|
|
34
|
-
const current = appliedKey.current;
|
|
35
|
-
if (current !== void 0) offsets.current.set(current, element.scrollTop);
|
|
36
|
-
};
|
|
37
|
-
element.addEventListener("scroll", record, { passive: true });
|
|
38
|
-
return () => element.removeEventListener("scroll", record);
|
|
24
|
+
const record = useCallback((event) => {
|
|
25
|
+
const current = appliedKey.current;
|
|
26
|
+
const element = event.currentTarget;
|
|
27
|
+
if (current !== void 0) offsets.current.set(current, element.scrollTop);
|
|
39
28
|
}, []);
|
|
29
|
+
const setNode = useCallback(
|
|
30
|
+
(element) => {
|
|
31
|
+
node.current?.removeEventListener("scroll", record);
|
|
32
|
+
node.current = element;
|
|
33
|
+
element?.addEventListener("scroll", record, { passive: true });
|
|
34
|
+
const ref = forwarded.current;
|
|
35
|
+
if (typeof ref === "function") ref(element);
|
|
36
|
+
else if (ref) ref.current = element;
|
|
37
|
+
},
|
|
38
|
+
[record]
|
|
39
|
+
);
|
|
40
40
|
useLayoutEffect(() => {
|
|
41
41
|
const element = node.current;
|
|
42
42
|
if (!element || key === void 0 || appliedKey.current === key) return;
|
|
@@ -96,4 +96,4 @@ var PageLayout = ({
|
|
|
96
96
|
export {
|
|
97
97
|
PageLayout
|
|
98
98
|
};
|
|
99
|
-
//# sourceMappingURL=chunk-
|
|
99
|
+
//# sourceMappingURL=chunk-IC2PIK3E.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/components/PageLayout/useScrollRestoration.ts","../src/components/PageLayout/PageLayout.component.tsx"],"sourcesContent":["import { useCallback, useEffect, useLayoutEffect, useRef } from 'react';\n\n/**\n * Remembers the content region's scroll offset per key, and applies the\n * stored offset whenever the key changes.\n *\n * The key is supplied by the consumer rather than derived here, because only\n * they know what counts as a navigation - this library has no router opinion.\n * That also decides the semantics, exactly as React Router's `getKey` does:\n * a per-path key gives \"this path always reopens where you left it\", while a\n * per-history-entry key gives the browser's own behaviour, where Back\n * restores but a fresh link to the same URL starts at the top.\n *\n * Returns a ref callback for the scroll container, which also forwards the\n * node to the caller's own `contentRef`.\n */\nexport const useScrollRestoration = (\n key: string | number | undefined,\n forwardTo: React.Ref<HTMLElement> | undefined,\n): ((node: HTMLElement | null) => void) => {\n const node = useRef<HTMLElement | null>(null);\n const offsets = useRef(new Map<string | number, number>());\n const appliedKey = useRef(key);\n\n // Held in a ref so the ref callback below can keep a stable identity. A\n // callback ref that changes identity is invoked with `null` and then the\n // node again on every render that changes it, which would detach and\n // reattach the caller's ref for no reason.\n const forwarded = useRef(forwardTo);\n useEffect(() => {\n forwarded.current = forwardTo;\n }, [forwardTo]);\n\n // Recorded as it happens rather than when the key changes: by the time a\n // navigation commits, the new page's content is already in the DOM and the\n // browser has clamped `scrollTop` to the new, possibly shorter, height. The\n // outgoing offset is no longer readable at that point.\n //\n // **The key is read from a ref, and the listener is registered once per\n // node.** Both halves matter. A listener re-subscribed per key closes over\n // the key it was created with, and restoring the incoming page's offset sets\n // `scrollTop`, which makes the browser dispatch a `scroll` event - so the\n // two race:\n //\n // 1. the layout effect below sets `scrollTop` for the incoming key\n // 2. passive effects swap the listener from the outgoing key to it\n // 3. the browser dispatches the scroll event from step 1\n //\n // React flushes passive effects through the scheduler and the browser\n // dispatches `scroll` at its own rendering opportunity, so nothing orders 2\n // before 3. When 3 won, the still-attached *outgoing* listener recorded the\n // incoming page's offset under the outgoing page's key - overwriting a real\n // offset with 0 and losing the position it exists to keep. It passed locally\n // and failed in CI, which is what a race of this shape looks like.\n //\n // Reading `appliedKey` instead means whichever order those steps take, the\n // offset is filed under the key that is actually on screen: the layout effect\n // updates the ref before it touches `scrollTop`.\n //\n // **Attached in the ref callback, not in an effect.** It used to be a\n // passive `useEffect`, which React runs some time *after* the commit - so a\n // scroll landing in between was never recorded, and returning to that page\n // restored 0. Storybook's production build starts a play function inside\n // that window (the dev server happens not to), which is how\n // `RestoresScrollPerKey` passed locally and failed on Chromatic, every time.\n // A ref callback runs during the commit itself, so the listener exists from\n // the moment the element does - and a swapped element is detached from and\n // re-attached to rather than silently left unobserved.\n const record = useCallback((event: Event) => {\n const current = appliedKey.current;\n const element = event.currentTarget as HTMLElement;\n if (current !== undefined) offsets.current.set(current, element.scrollTop);\n }, []);\n\n const setNode = useCallback(\n (element: HTMLElement | null) => {\n node.current?.removeEventListener('scroll', record);\n node.current = element;\n element?.addEventListener('scroll', record, { passive: true });\n\n const ref = forwarded.current;\n if (typeof ref === 'function') ref(element);\n else if (ref) (ref as React.RefObject<HTMLElement | null>).current = element;\n },\n [record],\n );\n\n // Layout effect so the offset is applied before paint - in a passive effect\n // the new page is visible at the previous page's offset for a frame first.\n // It does not run on the server, which is correct here: there is no scroll\n // position to restore in static markup, and the client applies it on mount.\n useLayoutEffect(() => {\n const element = node.current;\n if (!element || key === undefined || appliedKey.current === key) return;\n\n appliedKey.current = key;\n element.scrollTop = offsets.current.get(key) ?? 0;\n }, [key]);\n\n return setNode;\n};\n","import React from 'react';\nimport type { PageLayoutProps } from './PageLayout.types';\nimport './PageLayout.scss';\nimport { Toolbar } from '../Toolbar';\nimport { Header } from '../Header';\nimport { Footer } from '../Footer';\nimport type { FooterProps } from '../Footer';\nimport { Navigation } from '../Navigation';\nimport { useScrollRestoration } from './useScrollRestoration';\n\nconst defaultFooter: FooterProps = { copyright: `© ${new Date().getFullYear()} Eidos UI` };\n\nexport const PageLayout: React.FC<PageLayoutProps> = ({\n toolbar,\n navigation,\n header,\n footer,\n children,\n className = '',\n // Destructured so they cannot fall into `...rest` and land on the root\n // element, which is not the scroll container.\n contentRef,\n scrollRestorationKey,\n ...rest\n}) => {\n const classes = ['eidos-pagelayout', className].filter(Boolean).join(' ');\n const setContentRef = useScrollRestoration(scrollRestorationKey, contentRef);\n\n return (\n <div className={classes} {...rest}>\n {/* Spread rather than forwarded field by field, so a prop added to\n `Toolbar` reaches `PageLayout` consumers without a second edit\n here - the previous per-field list silently dropped anything it\n didn't know about. `className` is merged last so the layout's own\n grid-area class can't be overwritten by the caller's. */}\n <Toolbar\n {...toolbar}\n className={['eidos-pagelayout__toolbar', toolbar?.className].filter(Boolean).join(' ')}\n />\n <div className=\"eidos-pagelayout__navigation\">\n {navigation && <Navigation {...navigation} />}\n </div>\n {/* A scrollable region containing nothing focusable cannot be scrolled\n by keyboard at all, and a page body is often pure content - a\n report, an article. `tabIndex={0}` makes the region itself focusable\n so arrow keys and Page Up/Down reach it (SC 2.1.1), matching\n `VirtualList` and `Chat`. */}\n <main ref={setContentRef} tabIndex={0} className=\"eidos-pagelayout__content\">\n {header && (\n <Header\n {...header}\n className={['eidos-pagelayout__header', header.className].filter(Boolean).join(' ')}\n />\n )}\n <div className=\"eidos-pagelayout__body\">{children}</div>\n <Footer\n {...(footer ?? defaultFooter)}\n className={['eidos-pagelayout__footer', footer?.className].filter(Boolean).join(' ')}\n />\n </main>\n </div>\n );\n};\n"],"mappings":";;;;;;;;;;;;;;AAAA,SAAS,aAAa,WAAW,iBAAiB,cAAc;AAgBzD,IAAM,uBAAuB,CAClC,KACA,cACyC;AACzC,QAAM,OAAO,OAA2B,IAAI;AAC5C,QAAM,UAAU,OAAO,oBAAI,IAA6B,CAAC;AACzD,QAAM,aAAa,OAAO,GAAG;AAM7B,QAAM,YAAY,OAAO,SAAS;AAClC,YAAU,MAAM;AACd,cAAU,UAAU;AAAA,EACtB,GAAG,CAAC,SAAS,CAAC;AAqCd,QAAM,SAAS,YAAY,CAAC,UAAiB;AAC3C,UAAM,UAAU,WAAW;AAC3B,UAAM,UAAU,MAAM;AACtB,QAAI,YAAY,OAAW,SAAQ,QAAQ,IAAI,SAAS,QAAQ,SAAS;AAAA,EAC3E,GAAG,CAAC,CAAC;AAEL,QAAM,UAAU;AAAA,IACd,CAAC,YAAgC;AAC/B,WAAK,SAAS,oBAAoB,UAAU,MAAM;AAClD,WAAK,UAAU;AACf,eAAS,iBAAiB,UAAU,QAAQ,EAAE,SAAS,KAAK,CAAC;AAE7D,YAAM,MAAM,UAAU;AACtB,UAAI,OAAO,QAAQ,WAAY,KAAI,OAAO;AAAA,eACjC,IAAK,CAAC,IAA4C,UAAU;AAAA,IACvE;AAAA,IACA,CAAC,MAAM;AAAA,EACT;AAMA,kBAAgB,MAAM;AACpB,UAAM,UAAU,KAAK;AACrB,QAAI,CAAC,WAAW,QAAQ,UAAa,WAAW,YAAY,IAAK;AAEjE,eAAW,UAAU;AACrB,YAAQ,YAAY,QAAQ,QAAQ,IAAI,GAAG,KAAK;AAAA,EAClD,GAAG,CAAC,GAAG,CAAC;AAER,SAAO;AACT;;;ACjEM,cAYA,YAZA;AAzBN,IAAM,gBAA6B,EAAE,WAAW,SAAK,oBAAI,KAAK,GAAE,YAAY,CAAC,YAAY;AAElF,IAAM,aAAwC,CAAC;AAAA,EACpD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,YAAY;AAAA;AAAA;AAAA,EAGZ;AAAA,EACA;AAAA,EACA,GAAG;AACL,MAAM;AACJ,QAAM,UAAU,CAAC,oBAAoB,SAAS,EAAE,OAAO,OAAO,EAAE,KAAK,GAAG;AACxE,QAAM,gBAAgB,qBAAqB,sBAAsB,UAAU;AAE3E,SACE,qBAAC,SAAI,WAAW,SAAU,GAAG,MAM3B;AAAA;AAAA,MAAC;AAAA;AAAA,QACE,GAAG;AAAA,QACJ,WAAW,CAAC,6BAA6B,SAAS,SAAS,EAAE,OAAO,OAAO,EAAE,KAAK,GAAG;AAAA;AAAA,IACvF;AAAA,IACA,oBAAC,SAAI,WAAU,gCACZ,wBAAc,oBAAC,cAAY,GAAG,YAAY,GAC7C;AAAA,IAMA,qBAAC,UAAK,KAAK,eAAe,UAAU,GAAG,WAAU,6BAC9C;AAAA,gBACC;AAAA,QAAC;AAAA;AAAA,UACE,GAAG;AAAA,UACJ,WAAW,CAAC,4BAA4B,OAAO,SAAS,EAAE,OAAO,OAAO,EAAE,KAAK,GAAG;AAAA;AAAA,MACpF;AAAA,MAEF,oBAAC,SAAI,WAAU,0BAA0B,UAAS;AAAA,MAClD;AAAA,QAAC;AAAA;AAAA,UACE,GAAI,UAAU;AAAA,UACf,WAAW,CAAC,4BAA4B,QAAQ,SAAS,EAAE,OAAO,OAAO,EAAE,KAAK,GAAG;AAAA;AAAA,MACrF;AAAA,OACF;AAAA,KACF;AAEJ;","names":[]}
|
package/dist/index.cjs
CHANGED
|
@@ -62,7 +62,7 @@ var _chunk3VXUOAIJcjs = require('./chunk-3VXUOAIJ.cjs');
|
|
|
62
62
|
var _chunkVHO6JPRRcjs = require('./chunk-VHO6JPRR.cjs');
|
|
63
63
|
|
|
64
64
|
|
|
65
|
-
var
|
|
65
|
+
var _chunkGZDT2AHJcjs = require('./chunk-GZDT2AHJ.cjs');
|
|
66
66
|
|
|
67
67
|
|
|
68
68
|
var _chunkTNQ2ZZ7Wcjs = require('./chunk-TNQ2ZZ7W.cjs');
|
|
@@ -318,5 +318,5 @@ require('./chunk-DZUJEN5N.cjs');
|
|
|
318
318
|
|
|
319
319
|
|
|
320
320
|
|
|
321
|
-
exports.Accordion = _chunkEAJGSXEWcjs.Accordion; exports.AccordionItem = _chunkEAJGSXEWcjs.AccordionItem; exports.Alert = _chunkPE2SLXUScjs.Alert; exports.Avatar = _chunkQ3GNKF74cjs.Avatar; exports.AvatarGroup = _chunkQ3GNKF74cjs.AvatarGroup; exports.Badge = _chunk3ETWFS2Ncjs.Badge; exports.Breadcrumb = _chunkGP6EEZH5cjs.Breadcrumb; exports.Button = _chunkXPG56DLQcjs.Button; exports.ButtonGroup = _chunk2ROKOO22cjs.ButtonGroup; exports.Calendar = _chunkULKWUKD2cjs.Calendar; exports.Card = _chunkV5RS3JZNcjs.Card; exports.Chat = _chunkWTFEMOV5cjs.Chat; exports.Checkbox = _chunkYJNQLH2Lcjs.Checkbox; exports.Chip = _chunkWR77IZUUcjs.Chip; exports.ColorPicker = _chunkLUDK44NZcjs.ColorPicker; exports.Combobox = _chunkSLHIVEFWcjs.Combobox; exports.CommandPalette = _chunk7NUVLVSLcjs.CommandPalette; exports.CommentThread = _chunkWTFEMOV5cjs.CommentThread; exports.ContextMenu = _chunkAUNDBUHMcjs.ContextMenu; exports.DataGrid = _chunkHLI4SOH3cjs.DataGrid; exports.DatePicker = _chunkULKWUKD2cjs.DatePicker; exports.Divider = _chunk366R4GKRcjs.Divider; exports.Drawer = _chunkMKC6V3SYcjs.Drawer; exports.Dropdown = _chunkVXC7Z7XBcjs.Dropdown; exports.DropdownProvider = _chunkVXC7Z7XBcjs.DropdownProvider; exports.EmptyState = _chunkKV3V2CPHcjs.EmptyState; exports.FONT_ACCEPT = _chunkH46ZLPNJcjs.FONT_ACCEPT; exports.FileUpload = _chunkXRVRGRC3cjs.FileUpload; exports.Footer = _chunkPTBBY4ZXcjs.Footer; exports.Header = _chunkNFD7MPX4cjs.Header; exports.IconButton = _chunkXPG56DLQcjs.IconButton; exports.IdentityHeader = _chunkNFD7MPX4cjs.IdentityHeader; exports.InlineEdit = _chunkDG3O5NV4cjs.InlineEdit; exports.Input = _chunk4HZIXRAJcjs.Input; exports.Kbd = _chunkKVIL4EP4cjs.Kbd; exports.LinkProvider = _chunkXZILHRKGcjs.LinkProvider; exports.Menu = _chunkN5NXKVFJcjs.Menu; exports.MenuPanel = _chunkN5NXKVFJcjs.MenuPanel; exports.MessageComposer = _chunkWTFEMOV5cjs.MessageComposer; exports.Modal = _chunk6CW5QSYVcjs.Modal; exports.Navigation = _chunkF5IRMMZ2cjs.Navigation; exports.NumberInput = _chunk3VXUOAIJcjs.NumberInput; exports.OTPInput = _chunkVHO6JPRRcjs.OTPInput; exports.PageLayout =
|
|
321
|
+
exports.Accordion = _chunkEAJGSXEWcjs.Accordion; exports.AccordionItem = _chunkEAJGSXEWcjs.AccordionItem; exports.Alert = _chunkPE2SLXUScjs.Alert; exports.Avatar = _chunkQ3GNKF74cjs.Avatar; exports.AvatarGroup = _chunkQ3GNKF74cjs.AvatarGroup; exports.Badge = _chunk3ETWFS2Ncjs.Badge; exports.Breadcrumb = _chunkGP6EEZH5cjs.Breadcrumb; exports.Button = _chunkXPG56DLQcjs.Button; exports.ButtonGroup = _chunk2ROKOO22cjs.ButtonGroup; exports.Calendar = _chunkULKWUKD2cjs.Calendar; exports.Card = _chunkV5RS3JZNcjs.Card; exports.Chat = _chunkWTFEMOV5cjs.Chat; exports.Checkbox = _chunkYJNQLH2Lcjs.Checkbox; exports.Chip = _chunkWR77IZUUcjs.Chip; exports.ColorPicker = _chunkLUDK44NZcjs.ColorPicker; exports.Combobox = _chunkSLHIVEFWcjs.Combobox; exports.CommandPalette = _chunk7NUVLVSLcjs.CommandPalette; exports.CommentThread = _chunkWTFEMOV5cjs.CommentThread; exports.ContextMenu = _chunkAUNDBUHMcjs.ContextMenu; exports.DataGrid = _chunkHLI4SOH3cjs.DataGrid; exports.DatePicker = _chunkULKWUKD2cjs.DatePicker; exports.Divider = _chunk366R4GKRcjs.Divider; exports.Drawer = _chunkMKC6V3SYcjs.Drawer; exports.Dropdown = _chunkVXC7Z7XBcjs.Dropdown; exports.DropdownProvider = _chunkVXC7Z7XBcjs.DropdownProvider; exports.EmptyState = _chunkKV3V2CPHcjs.EmptyState; exports.FONT_ACCEPT = _chunkH46ZLPNJcjs.FONT_ACCEPT; exports.FileUpload = _chunkXRVRGRC3cjs.FileUpload; exports.Footer = _chunkPTBBY4ZXcjs.Footer; exports.Header = _chunkNFD7MPX4cjs.Header; exports.IconButton = _chunkXPG56DLQcjs.IconButton; exports.IdentityHeader = _chunkNFD7MPX4cjs.IdentityHeader; exports.InlineEdit = _chunkDG3O5NV4cjs.InlineEdit; exports.Input = _chunk4HZIXRAJcjs.Input; exports.Kbd = _chunkKVIL4EP4cjs.Kbd; exports.LinkProvider = _chunkXZILHRKGcjs.LinkProvider; exports.Menu = _chunkN5NXKVFJcjs.Menu; exports.MenuPanel = _chunkN5NXKVFJcjs.MenuPanel; exports.MessageComposer = _chunkWTFEMOV5cjs.MessageComposer; exports.Modal = _chunk6CW5QSYVcjs.Modal; exports.Navigation = _chunkF5IRMMZ2cjs.Navigation; exports.NumberInput = _chunk3VXUOAIJcjs.NumberInput; exports.OTPInput = _chunkVHO6JPRRcjs.OTPInput; exports.PageLayout = _chunkGZDT2AHJcjs.PageLayout; exports.Pagination = _chunk6IB27I5Kcjs.Pagination; exports.Pill = _chunkEQ6BNPTLcjs.Pill; exports.Popover = _chunkYIOHKQ6Ycjs.Popover; exports.Progress = _chunkV2S6XWIQcjs.Progress; exports.Radio = _chunkKPMIIGHHcjs.Radio; exports.RadioGroup = _chunkKPMIIGHHcjs.RadioGroup; exports.SegmentedControl = _chunkJFV56ISScjs.SegmentedControl; exports.Select = _chunkB2OHBIALcjs.Select; exports.Skeleton = _chunkIGFR5TEScjs.Skeleton; exports.Slider = _chunkOZZHOEN5cjs.Slider; exports.SnackbarContainer = _chunkNERASTO2cjs.SnackbarContainer; exports.SnackbarProvider = _chunkNERASTO2cjs.SnackbarProvider; exports.Spinner = _chunkTROYIUDHcjs.Spinner; exports.SplitButton = _chunk7FV2X62Wcjs.SplitButton; exports.SplitChip = _chunkLWCFT5SPcjs.SplitChip; exports.Stepper = _chunkHFZ5FKRGcjs.Stepper; exports.Switch = _chunkIXZMKIV5cjs.Switch; exports.Tab = _chunkCFVUUMQAcjs.Tab; exports.TabPanel = _chunkCFVUUMQAcjs.TabPanel; exports.Table = _chunkNNAEQR3Ycjs.Table; exports.TableFiltersDropdown = _chunk544RAXWOcjs.TableFiltersDropdown; exports.Tabs = _chunkCFVUUMQAcjs.Tabs; exports.TagInput = _chunkHR5MWA3Bcjs.TagInput; exports.Textarea = _chunkUUT2VEN5cjs.Textarea; exports.ThemeEditor = _chunkOGGZIFTIcjs.ThemeEditor; exports.ThemeProvider = _chunkH46ZLPNJcjs.ThemeProvider; exports.TimeInput = _chunkULKWUKD2cjs.TimeInput; exports.Timeline = _chunkVYF7WV4Kcjs.Timeline; exports.Toolbar = _chunkTNQ2ZZ7Wcjs.Toolbar; exports.Tooltip = _chunkUTYY35QXcjs.Tooltip; exports.TreeView = _chunkXKQMEVODcjs.TreeView; exports.VirtualList = _chunkRUDXHT76cjs.VirtualList; exports.defaultTheme = _chunkH46ZLPNJcjs.defaultTheme; exports.familyNameFromFile = _chunkH46ZLPNJcjs.familyNameFromFile; exports.isFontAvailable = _chunkH46ZLPNJcjs.isFontAvailable; exports.isFontStackAvailable = _chunkH46ZLPNJcjs.isFontStackAvailable; exports.registerFontFace = _chunkH46ZLPNJcjs.registerFontFace; exports.registerFontFile = _chunkH46ZLPNJcjs.registerFontFile; exports.registerIcons = _chunkCJT7L764cjs.registerIcons; exports.renderIcon = _chunkENMDOQQOcjs.renderIcon; exports.toFontStack = _chunkH46ZLPNJcjs.toFontStack; exports.useDropdownContext = _chunkVXC7Z7XBcjs.useDropdownContext; exports.useLinkComponent = _chunk75Q5OS7Lcjs.useLinkComponent; exports.useSnackbar = _chunkNERASTO2cjs.useSnackbar; exports.useSnackbarContext = _chunkNERASTO2cjs.useSnackbarContext; exports.useTheme = _chunkH46ZLPNJcjs.useTheme;
|
|
322
322
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";Object.defineProperty(exports, "__esModule", {value: true});
|
|
2
2
|
|
|
3
|
-
var
|
|
3
|
+
var _chunkGZDT2AHJcjs = require('../chunk-GZDT2AHJ.cjs');
|
|
4
4
|
require('../chunk-TNQ2ZZ7W.cjs');
|
|
5
5
|
require('../chunk-NFD7MPX4.cjs');
|
|
6
6
|
require('../chunk-YIOHKQ6Y.cjs');
|
|
@@ -24,5 +24,5 @@ require('../chunk-75Q5OS7L.cjs');
|
|
|
24
24
|
require('../chunk-DZUJEN5N.cjs');
|
|
25
25
|
|
|
26
26
|
|
|
27
|
-
exports.PageLayout =
|
|
27
|
+
exports.PageLayout = _chunkGZDT2AHJcjs.PageLayout;
|
|
28
28
|
//# sourceMappingURL=index.cjs.map
|
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/components/PageLayout/useScrollRestoration.ts","../src/components/PageLayout/PageLayout.component.tsx"],"sourcesContent":["import { useCallback, useEffect, useLayoutEffect, useRef } from 'react';\n\n/**\n * Remembers the content region's scroll offset per key, and applies the\n * stored offset whenever the key changes.\n *\n * The key is supplied by the consumer rather than derived here, because only\n * they know what counts as a navigation - this library has no router opinion.\n * That also decides the semantics, exactly as React Router's `getKey` does:\n * a per-path key gives \"this path always reopens where you left it\", while a\n * per-history-entry key gives the browser's own behaviour, where Back\n * restores but a fresh link to the same URL starts at the top.\n *\n * Returns a ref callback for the scroll container, which also forwards the\n * node to the caller's own `contentRef`.\n */\nexport const useScrollRestoration = (\n key: string | number | undefined,\n forwardTo: React.Ref<HTMLElement> | undefined,\n): ((node: HTMLElement | null) => void) => {\n const node = useRef<HTMLElement | null>(null);\n const offsets = useRef(new Map<string | number, number>());\n const appliedKey = useRef(key);\n\n // Held in a ref so the ref callback below can keep a stable identity. A\n // callback ref that changes identity is invoked with `null` and then the\n // node again on every render that changes it, which would detach and\n // reattach the caller's ref for no reason.\n const forwarded = useRef(forwardTo);\n useEffect(() => {\n forwarded.current = forwardTo;\n }, [forwardTo]);\n\n const setNode = useCallback((element: HTMLElement | null) => {\n node.current = element;\n const ref = forwarded.current;\n if (typeof ref === 'function') ref(element);\n else if (ref) (ref as React.RefObject<HTMLElement | null>).current = element;\n }, []);\n\n // Recorded as it happens rather than when the key changes: by the time a\n // navigation commits, the new page's content is already in the DOM and the\n // browser has clamped `scrollTop` to the new, possibly shorter, height. The\n // outgoing offset is no longer readable at that point.\n //\n // **The key is read from a ref, and the listener is registered once.** Both\n // halves matter. A listener re-subscribed per key closes over the key it was\n // created with, and restoring the incoming page's offset sets `scrollTop`,\n // which makes the browser dispatch a `scroll` event - so the two race:\n //\n // 1. the layout effect below sets `scrollTop` for the incoming key\n // 2. passive effects swap the listener from the outgoing key to it\n // 3. the browser dispatches the scroll event from step 1\n //\n // React flushes passive effects through the scheduler and the browser\n // dispatches `scroll` at its own rendering opportunity, so nothing orders 2\n // before 3. When 3 won, the still-attached *outgoing* listener recorded the\n // incoming page's offset under the outgoing page's key - overwriting a real\n // offset with 0 and losing the position it exists to keep. It passed locally\n // and failed in CI, which is what a race of this shape looks like.\n //\n // Reading `appliedKey` instead means whichever order those steps take, the\n // offset is filed under the key that is actually on screen: the layout effect\n // updates the ref before it touches `scrollTop`.\n useEffect(() => {\n const element = node.current;\n if (!element) return;\n\n const record = () => {\n const current = appliedKey.current;\n if (current !== undefined) offsets.current.set(current, element.scrollTop);\n };\n\n element.addEventListener('scroll', record, { passive: true });\n return () => element.removeEventListener('scroll', record);\n // Registered once, for the same reason: re-subscribing is what created the\n // window where a stale key could be recorded.\n }, []);\n\n // Layout effect so the offset is applied before paint - in a passive effect\n // the new page is visible at the previous page's offset for a frame first.\n // It does not run on the server, which is correct here: there is no scroll\n // position to restore in static markup, and the client applies it on mount.\n useLayoutEffect(() => {\n const element = node.current;\n if (!element || key === undefined || appliedKey.current === key) return;\n\n appliedKey.current = key;\n element.scrollTop = offsets.current.get(key) ?? 0;\n }, [key]);\n\n return setNode;\n};\n","import React from 'react';\nimport type { PageLayoutProps } from './PageLayout.types';\nimport './PageLayout.scss';\nimport { Toolbar } from '../Toolbar';\nimport { Header } from '../Header';\nimport { Footer } from '../Footer';\nimport type { FooterProps } from '../Footer';\nimport { Navigation } from '../Navigation';\nimport { useScrollRestoration } from './useScrollRestoration';\n\nconst defaultFooter: FooterProps = { copyright: `© ${new Date().getFullYear()} Eidos UI` };\n\nexport const PageLayout: React.FC<PageLayoutProps> = ({\n toolbar,\n navigation,\n header,\n footer,\n children,\n className = '',\n // Destructured so they cannot fall into `...rest` and land on the root\n // element, which is not the scroll container.\n contentRef,\n scrollRestorationKey,\n ...rest\n}) => {\n const classes = ['eidos-pagelayout', className].filter(Boolean).join(' ');\n const setContentRef = useScrollRestoration(scrollRestorationKey, contentRef);\n\n return (\n <div className={classes} {...rest}>\n {/* Spread rather than forwarded field by field, so a prop added to\n `Toolbar` reaches `PageLayout` consumers without a second edit\n here - the previous per-field list silently dropped anything it\n didn't know about. `className` is merged last so the layout's own\n grid-area class can't be overwritten by the caller's. */}\n <Toolbar\n {...toolbar}\n className={['eidos-pagelayout__toolbar', toolbar?.className].filter(Boolean).join(' ')}\n />\n <div className=\"eidos-pagelayout__navigation\">\n {navigation && <Navigation {...navigation} />}\n </div>\n {/* A scrollable region containing nothing focusable cannot be scrolled\n by keyboard at all, and a page body is often pure content - a\n report, an article. `tabIndex={0}` makes the region itself focusable\n so arrow keys and Page Up/Down reach it (SC 2.1.1), matching\n `VirtualList` and `Chat`. */}\n <main ref={setContentRef} tabIndex={0} className=\"eidos-pagelayout__content\">\n {header && (\n <Header\n {...header}\n className={['eidos-pagelayout__header', header.className].filter(Boolean).join(' ')}\n />\n )}\n <div className=\"eidos-pagelayout__body\">{children}</div>\n <Footer\n {...(footer ?? defaultFooter)}\n className={['eidos-pagelayout__footer', footer?.className].filter(Boolean).join(' ')}\n />\n </main>\n </div>\n );\n};\n"],"mappings":";;;;;;;;;;;;;;AAAA,SAAS,aAAa,WAAW,iBAAiB,cAAc;AAgBzD,IAAM,uBAAuB,CAClC,KACA,cACyC;AACzC,QAAM,OAAO,OAA2B,IAAI;AAC5C,QAAM,UAAU,OAAO,oBAAI,IAA6B,CAAC;AACzD,QAAM,aAAa,OAAO,GAAG;AAM7B,QAAM,YAAY,OAAO,SAAS;AAClC,YAAU,MAAM;AACd,cAAU,UAAU;AAAA,EACtB,GAAG,CAAC,SAAS,CAAC;AAEd,QAAM,UAAU,YAAY,CAAC,YAAgC;AAC3D,SAAK,UAAU;AACf,UAAM,MAAM,UAAU;AACtB,QAAI,OAAO,QAAQ,WAAY,KAAI,OAAO;AAAA,aACjC,IAAK,CAAC,IAA4C,UAAU;AAAA,EACvE,GAAG,CAAC,CAAC;AA0BL,YAAU,MAAM;AACd,UAAM,UAAU,KAAK;AACrB,QAAI,CAAC,QAAS;AAEd,UAAM,SAAS,MAAM;AACnB,YAAM,UAAU,WAAW;AAC3B,UAAI,YAAY,OAAW,SAAQ,QAAQ,IAAI,SAAS,QAAQ,SAAS;AAAA,IAC3E;AAEA,YAAQ,iBAAiB,UAAU,QAAQ,EAAE,SAAS,KAAK,CAAC;AAC5D,WAAO,MAAM,QAAQ,oBAAoB,UAAU,MAAM;AAAA,EAG3D,GAAG,CAAC,CAAC;AAML,kBAAgB,MAAM;AACpB,UAAM,UAAU,KAAK;AACrB,QAAI,CAAC,WAAW,QAAQ,UAAa,WAAW,YAAY,IAAK;AAEjE,eAAW,UAAU;AACrB,YAAQ,YAAY,QAAQ,QAAQ,IAAI,GAAG,KAAK;AAAA,EAClD,GAAG,CAAC,GAAG,CAAC;AAER,SAAO;AACT;;;ACzDM,cAYA,YAZA;AAzBN,IAAM,gBAA6B,EAAE,WAAW,SAAK,oBAAI,KAAK,GAAE,YAAY,CAAC,YAAY;AAElF,IAAM,aAAwC,CAAC;AAAA,EACpD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,YAAY;AAAA;AAAA;AAAA,EAGZ;AAAA,EACA;AAAA,EACA,GAAG;AACL,MAAM;AACJ,QAAM,UAAU,CAAC,oBAAoB,SAAS,EAAE,OAAO,OAAO,EAAE,KAAK,GAAG;AACxE,QAAM,gBAAgB,qBAAqB,sBAAsB,UAAU;AAE3E,SACE,qBAAC,SAAI,WAAW,SAAU,GAAG,MAM3B;AAAA;AAAA,MAAC;AAAA;AAAA,QACE,GAAG;AAAA,QACJ,WAAW,CAAC,6BAA6B,SAAS,SAAS,EAAE,OAAO,OAAO,EAAE,KAAK,GAAG;AAAA;AAAA,IACvF;AAAA,IACA,oBAAC,SAAI,WAAU,gCACZ,wBAAc,oBAAC,cAAY,GAAG,YAAY,GAC7C;AAAA,IAMA,qBAAC,UAAK,KAAK,eAAe,UAAU,GAAG,WAAU,6BAC9C;AAAA,gBACC;AAAA,QAAC;AAAA;AAAA,UACE,GAAG;AAAA,UACJ,WAAW,CAAC,4BAA4B,OAAO,SAAS,EAAE,OAAO,OAAO,EAAE,KAAK,GAAG;AAAA;AAAA,MACpF;AAAA,MAEF,oBAAC,SAAI,WAAU,0BAA0B,UAAS;AAAA,MAClD;AAAA,QAAC;AAAA;AAAA,UACE,GAAI,UAAU;AAAA,UACf,WAAW,CAAC,4BAA4B,QAAQ,SAAS,EAAE,OAAO,OAAO,EAAE,KAAK,GAAG;AAAA;AAAA,MACrF;AAAA,OACF;AAAA,KACF;AAEJ;","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["/home/runner/work/eidos-ui/eidos-ui/dist/chunk-QUSO4GFA.cjs","../src/components/PageLayout/useScrollRestoration.ts","../src/components/PageLayout/PageLayout.component.tsx"],"names":[],"mappings":"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","file":"/home/runner/work/eidos-ui/eidos-ui/dist/chunk-QUSO4GFA.cjs","sourcesContent":[null,"import { useCallback, useEffect, useLayoutEffect, useRef } from 'react';\n\n/**\n * Remembers the content region's scroll offset per key, and applies the\n * stored offset whenever the key changes.\n *\n * The key is supplied by the consumer rather than derived here, because only\n * they know what counts as a navigation - this library has no router opinion.\n * That also decides the semantics, exactly as React Router's `getKey` does:\n * a per-path key gives \"this path always reopens where you left it\", while a\n * per-history-entry key gives the browser's own behaviour, where Back\n * restores but a fresh link to the same URL starts at the top.\n *\n * Returns a ref callback for the scroll container, which also forwards the\n * node to the caller's own `contentRef`.\n */\nexport const useScrollRestoration = (\n key: string | number | undefined,\n forwardTo: React.Ref<HTMLElement> | undefined,\n): ((node: HTMLElement | null) => void) => {\n const node = useRef<HTMLElement | null>(null);\n const offsets = useRef(new Map<string | number, number>());\n const appliedKey = useRef(key);\n\n // Held in a ref so the ref callback below can keep a stable identity. A\n // callback ref that changes identity is invoked with `null` and then the\n // node again on every render that changes it, which would detach and\n // reattach the caller's ref for no reason.\n const forwarded = useRef(forwardTo);\n useEffect(() => {\n forwarded.current = forwardTo;\n }, [forwardTo]);\n\n const setNode = useCallback((element: HTMLElement | null) => {\n node.current = element;\n const ref = forwarded.current;\n if (typeof ref === 'function') ref(element);\n else if (ref) (ref as React.RefObject<HTMLElement | null>).current = element;\n }, []);\n\n // Recorded as it happens rather than when the key changes: by the time a\n // navigation commits, the new page's content is already in the DOM and the\n // browser has clamped `scrollTop` to the new, possibly shorter, height. The\n // outgoing offset is no longer readable at that point.\n //\n // **The key is read from a ref, and the listener is registered once.** Both\n // halves matter. A listener re-subscribed per key closes over the key it was\n // created with, and restoring the incoming page's offset sets `scrollTop`,\n // which makes the browser dispatch a `scroll` event - so the two race:\n //\n // 1. the layout effect below sets `scrollTop` for the incoming key\n // 2. passive effects swap the listener from the outgoing key to it\n // 3. the browser dispatches the scroll event from step 1\n //\n // React flushes passive effects through the scheduler and the browser\n // dispatches `scroll` at its own rendering opportunity, so nothing orders 2\n // before 3. When 3 won, the still-attached *outgoing* listener recorded the\n // incoming page's offset under the outgoing page's key - overwriting a real\n // offset with 0 and losing the position it exists to keep. It passed locally\n // and failed in CI, which is what a race of this shape looks like.\n //\n // Reading `appliedKey` instead means whichever order those steps take, the\n // offset is filed under the key that is actually on screen: the layout effect\n // updates the ref before it touches `scrollTop`.\n useEffect(() => {\n const element = node.current;\n if (!element) return;\n\n const record = () => {\n const current = appliedKey.current;\n if (current !== undefined) offsets.current.set(current, element.scrollTop);\n };\n\n element.addEventListener('scroll', record, { passive: true });\n return () => element.removeEventListener('scroll', record);\n // Registered once, for the same reason: re-subscribing is what created the\n // window where a stale key could be recorded.\n }, []);\n\n // Layout effect so the offset is applied before paint - in a passive effect\n // the new page is visible at the previous page's offset for a frame first.\n // It does not run on the server, which is correct here: there is no scroll\n // position to restore in static markup, and the client applies it on mount.\n useLayoutEffect(() => {\n const element = node.current;\n if (!element || key === undefined || appliedKey.current === key) return;\n\n appliedKey.current = key;\n element.scrollTop = offsets.current.get(key) ?? 0;\n }, [key]);\n\n return setNode;\n};\n","import React from 'react';\nimport type { PageLayoutProps } from './PageLayout.types';\nimport './PageLayout.scss';\nimport { Toolbar } from '../Toolbar';\nimport { Header } from '../Header';\nimport { Footer } from '../Footer';\nimport type { FooterProps } from '../Footer';\nimport { Navigation } from '../Navigation';\nimport { useScrollRestoration } from './useScrollRestoration';\n\nconst defaultFooter: FooterProps = { copyright: `© ${new Date().getFullYear()} Eidos UI` };\n\nexport const PageLayout: React.FC<PageLayoutProps> = ({\n toolbar,\n navigation,\n header,\n footer,\n children,\n className = '',\n // Destructured so they cannot fall into `...rest` and land on the root\n // element, which is not the scroll container.\n contentRef,\n scrollRestorationKey,\n ...rest\n}) => {\n const classes = ['eidos-pagelayout', className].filter(Boolean).join(' ');\n const setContentRef = useScrollRestoration(scrollRestorationKey, contentRef);\n\n return (\n <div className={classes} {...rest}>\n {/* Spread rather than forwarded field by field, so a prop added to\n `Toolbar` reaches `PageLayout` consumers without a second edit\n here - the previous per-field list silently dropped anything it\n didn't know about. `className` is merged last so the layout's own\n grid-area class can't be overwritten by the caller's. */}\n <Toolbar\n {...toolbar}\n className={['eidos-pagelayout__toolbar', toolbar?.className].filter(Boolean).join(' ')}\n />\n <div className=\"eidos-pagelayout__navigation\">\n {navigation && <Navigation {...navigation} />}\n </div>\n {/* A scrollable region containing nothing focusable cannot be scrolled\n by keyboard at all, and a page body is often pure content - a\n report, an article. `tabIndex={0}` makes the region itself focusable\n so arrow keys and Page Up/Down reach it (SC 2.1.1), matching\n `VirtualList` and `Chat`. */}\n <main ref={setContentRef} tabIndex={0} className=\"eidos-pagelayout__content\">\n {header && (\n <Header\n {...header}\n className={['eidos-pagelayout__header', header.className].filter(Boolean).join(' ')}\n />\n )}\n <div className=\"eidos-pagelayout__body\">{children}</div>\n <Footer\n {...(footer ?? defaultFooter)}\n className={['eidos-pagelayout__footer', footer?.className].filter(Boolean).join(' ')}\n />\n </main>\n </div>\n );\n};\n"]}
|