@byline/host-tanstack-start 4.17.0 → 4.18.0
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/admin-shell/chrome/router-pager.d.ts +7 -0
- package/dist/admin-shell/chrome/router-pager.js +13 -0
- package/dist/admin-shell/chrome/scroll-to-top.d.ts +20 -0
- package/dist/admin-shell/chrome/scroll-to-top.js +9 -0
- package/package.json +10 -10
- package/src/admin-shell/chrome/router-pager.tsx +22 -0
- package/src/admin-shell/chrome/scroll-to-top.test.ts +56 -0
- package/src/admin-shell/chrome/scroll-to-top.ts +28 -0
|
@@ -13,6 +13,13 @@ interface RouterPageProps extends PaginationProps {
|
|
|
13
13
|
* A convenience pager wrapped around uikit's Pagination with `asChild`
|
|
14
14
|
* targets that delegate to TanStack Router's `<Link>`. `asChild` merges
|
|
15
15
|
* the existing button styles into the rendered Link.
|
|
16
|
+
*
|
|
17
|
+
* `smoothScrollToTop` is for a pager sitting *below* a long list: turning a
|
|
18
|
+
* page there should carry the reader back to the top rather than leaving them
|
|
19
|
+
* at the foot of a fresh page. It suppresses the router's own instant
|
|
20
|
+
* `resetScroll` and animates instead — so the component owes an actual
|
|
21
|
+
* scroll, which was the missing half: the flag disabled the reset and put
|
|
22
|
+
* nothing in its place, so a bottom pager did not scroll at all.
|
|
16
23
|
*/
|
|
17
24
|
export declare function RouterPager({ className, smoothScrollToTop, 'aria-label': ariaLabel, ...rest }: RouterPageProps): React.JSX.Element;
|
|
18
25
|
export {};
|
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback } from "react";
|
|
2
3
|
import { useRouterState } from "@tanstack/react-router";
|
|
3
4
|
import { ChevronLeftDoubleIcon, ChevronLeftIcon, ChevronRightDoubleIcon, ChevronRightIcon, Pagination } from "@byline/ui/react";
|
|
4
5
|
import { Link } from "./loose-router.js";
|
|
6
|
+
import { scrollWindowToTop } from "./scroll-to-top.js";
|
|
5
7
|
function RouterPager({ className, smoothScrollToTop, 'aria-label': ariaLabel, ...rest }) {
|
|
6
8
|
const location = useRouterState({
|
|
7
9
|
select: (s)=>s.location
|
|
8
10
|
});
|
|
11
|
+
const handleScrollToTop = useCallback(()=>{
|
|
12
|
+
if (true !== smoothScrollToTop) return;
|
|
13
|
+
scrollWindowToTop();
|
|
14
|
+
}, [
|
|
15
|
+
smoothScrollToTop
|
|
16
|
+
]);
|
|
9
17
|
return /*#__PURE__*/ jsx(Pagination, {
|
|
10
18
|
variant: "dashboard",
|
|
11
19
|
...rest,
|
|
@@ -19,6 +27,7 @@ function RouterPager({ className, smoothScrollToTop, 'aria-label': ariaLabel, ..
|
|
|
19
27
|
return /*#__PURE__*/ jsx(Pagination.First, {
|
|
20
28
|
render: item.disabled ? /*#__PURE__*/ jsx("div", {}) : /*#__PURE__*/ jsx(Link, {
|
|
21
29
|
resetScroll: true !== smoothScrollToTop,
|
|
30
|
+
onClick: handleScrollToTop,
|
|
22
31
|
to: location.pathname,
|
|
23
32
|
search: params
|
|
24
33
|
}),
|
|
@@ -32,6 +41,7 @@ function RouterPager({ className, smoothScrollToTop, 'aria-label': ariaLabel, ..
|
|
|
32
41
|
return /*#__PURE__*/ jsx(Pagination.Previous, {
|
|
33
42
|
render: item.disabled ? /*#__PURE__*/ jsx("div", {}) : /*#__PURE__*/ jsx(Link, {
|
|
34
43
|
resetScroll: true !== smoothScrollToTop,
|
|
44
|
+
onClick: handleScrollToTop,
|
|
35
45
|
to: location.pathname,
|
|
36
46
|
search: params
|
|
37
47
|
}),
|
|
@@ -46,6 +56,7 @@ function RouterPager({ className, smoothScrollToTop, 'aria-label': ariaLabel, ..
|
|
|
46
56
|
return /*#__PURE__*/ jsx(Pagination.Number, {
|
|
47
57
|
render: item.disabled ? /*#__PURE__*/ jsx("div", {}) : /*#__PURE__*/ jsx(Link, {
|
|
48
58
|
resetScroll: true !== smoothScrollToTop,
|
|
59
|
+
onClick: handleScrollToTop,
|
|
49
60
|
to: location.pathname,
|
|
50
61
|
search: params
|
|
51
62
|
}),
|
|
@@ -61,6 +72,7 @@ function RouterPager({ className, smoothScrollToTop, 'aria-label': ariaLabel, ..
|
|
|
61
72
|
return /*#__PURE__*/ jsx(Pagination.Next, {
|
|
62
73
|
render: item.disabled ? /*#__PURE__*/ jsx("div", {}) : /*#__PURE__*/ jsx(Link, {
|
|
63
74
|
resetScroll: true !== smoothScrollToTop,
|
|
75
|
+
onClick: handleScrollToTop,
|
|
64
76
|
to: location.pathname,
|
|
65
77
|
search: params
|
|
66
78
|
}),
|
|
@@ -75,6 +87,7 @@ function RouterPager({ className, smoothScrollToTop, 'aria-label': ariaLabel, ..
|
|
|
75
87
|
return /*#__PURE__*/ jsx(Pagination.Last, {
|
|
76
88
|
render: item.disabled ? /*#__PURE__*/ jsx("div", {}) : /*#__PURE__*/ jsx(Link, {
|
|
77
89
|
resetScroll: true !== smoothScrollToTop,
|
|
90
|
+
onClick: handleScrollToTop,
|
|
78
91
|
to: location.pathname,
|
|
79
92
|
search: params
|
|
80
93
|
}),
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Return the reader to the top of the page.
|
|
10
|
+
*
|
|
11
|
+
* Separate from `RouterPager` so the behaviour can be asserted without
|
|
12
|
+
* mounting a router. `smoothScrollToTop` suppresses TanStack's instant
|
|
13
|
+
* `resetScroll` in favour of an animated one, which makes a missing
|
|
14
|
+
* implementation silent — the page simply does not move. That is what
|
|
15
|
+
* happened to every pager carrying the flag.
|
|
16
|
+
*
|
|
17
|
+
* Honours `prefers-reduced-motion`: those readers still arrive at the top,
|
|
18
|
+
* without the travel.
|
|
19
|
+
*/
|
|
20
|
+
export declare function scrollWindowToTop(): void;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
function scrollWindowToTop() {
|
|
2
|
+
if ("u" < typeof window) return;
|
|
3
|
+
const prefersReducedMotion = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;
|
|
4
|
+
window.scrollTo({
|
|
5
|
+
top: 0,
|
|
6
|
+
behavior: true === prefersReducedMotion ? 'auto' : 'smooth'
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
export { scrollWindowToTop };
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"private": false,
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MPL-2.0",
|
|
6
|
-
"version": "4.
|
|
6
|
+
"version": "4.18.0",
|
|
7
7
|
"engines": {
|
|
8
8
|
"node": ">=20.9.0"
|
|
9
9
|
},
|
|
@@ -125,15 +125,15 @@
|
|
|
125
125
|
"react-json-view-lite": "^2.5.0",
|
|
126
126
|
"react-swipeable": "^7.0.2",
|
|
127
127
|
"zod": "^4.4.3",
|
|
128
|
-
"@byline/
|
|
129
|
-
"@byline/ai": "4.
|
|
130
|
-
"@byline/
|
|
131
|
-
"@byline/
|
|
132
|
-
"@byline/
|
|
133
|
-
"@byline/
|
|
134
|
-
"@byline/
|
|
135
|
-
"@byline/
|
|
136
|
-
"@byline/
|
|
128
|
+
"@byline/admin": "4.18.0",
|
|
129
|
+
"@byline/ai": "4.18.0",
|
|
130
|
+
"@byline/analytics-agent": "4.18.0",
|
|
131
|
+
"@byline/client": "4.18.0",
|
|
132
|
+
"@byline/auth": "4.18.0",
|
|
133
|
+
"@byline/analytics": "4.18.0",
|
|
134
|
+
"@byline/i18n": "4.18.0",
|
|
135
|
+
"@byline/core": "4.18.0",
|
|
136
|
+
"@byline/ui": "4.18.0"
|
|
137
137
|
},
|
|
138
138
|
"peerDependencies": {
|
|
139
139
|
"@tanstack/react-router": "^1.167.0",
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* Copyright (c) Infonomic Company Limited
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { useCallback } from 'react'
|
|
9
10
|
import { useRouterState } from '@tanstack/react-router'
|
|
10
11
|
|
|
11
12
|
import type { PaginationProps } from '@byline/ui/react'
|
|
@@ -18,6 +19,7 @@ import {
|
|
|
18
19
|
} from '@byline/ui/react'
|
|
19
20
|
|
|
20
21
|
import { Link } from './loose-router.js'
|
|
22
|
+
import { scrollWindowToTop } from './scroll-to-top.js'
|
|
21
23
|
|
|
22
24
|
interface RouterPageProps extends PaginationProps {
|
|
23
25
|
smoothScrollToTop?: boolean
|
|
@@ -27,6 +29,13 @@ interface RouterPageProps extends PaginationProps {
|
|
|
27
29
|
* A convenience pager wrapped around uikit's Pagination with `asChild`
|
|
28
30
|
* targets that delegate to TanStack Router's `<Link>`. `asChild` merges
|
|
29
31
|
* the existing button styles into the rendered Link.
|
|
32
|
+
*
|
|
33
|
+
* `smoothScrollToTop` is for a pager sitting *below* a long list: turning a
|
|
34
|
+
* page there should carry the reader back to the top rather than leaving them
|
|
35
|
+
* at the foot of a fresh page. It suppresses the router's own instant
|
|
36
|
+
* `resetScroll` and animates instead — so the component owes an actual
|
|
37
|
+
* scroll, which was the missing half: the flag disabled the reset and put
|
|
38
|
+
* nothing in its place, so a bottom pager did not scroll at all.
|
|
30
39
|
*/
|
|
31
40
|
export function RouterPager({
|
|
32
41
|
className,
|
|
@@ -36,6 +45,14 @@ export function RouterPager({
|
|
|
36
45
|
}: RouterPageProps): React.JSX.Element {
|
|
37
46
|
const location = useRouterState({ select: (s) => s.location })
|
|
38
47
|
|
|
48
|
+
// Fires alongside navigation rather than after it: the scroll and the new
|
|
49
|
+
// page load run together, so the list is in place by the time the reader
|
|
50
|
+
// arrives at the top.
|
|
51
|
+
const handleScrollToTop = useCallback(() => {
|
|
52
|
+
if (smoothScrollToTop !== true) return
|
|
53
|
+
scrollWindowToTop()
|
|
54
|
+
}, [smoothScrollToTop])
|
|
55
|
+
|
|
39
56
|
return (
|
|
40
57
|
<Pagination variant="dashboard" {...rest}>
|
|
41
58
|
<Pagination.Root className={className} ariaLabel={ariaLabel}>
|
|
@@ -51,6 +68,7 @@ export function RouterPager({
|
|
|
51
68
|
) : (
|
|
52
69
|
<Link
|
|
53
70
|
resetScroll={smoothScrollToTop !== true}
|
|
71
|
+
onClick={handleScrollToTop}
|
|
54
72
|
to={location.pathname as never}
|
|
55
73
|
search={params}
|
|
56
74
|
/>
|
|
@@ -76,6 +94,7 @@ export function RouterPager({
|
|
|
76
94
|
) : (
|
|
77
95
|
<Link
|
|
78
96
|
resetScroll={smoothScrollToTop !== true}
|
|
97
|
+
onClick={handleScrollToTop}
|
|
79
98
|
to={location.pathname as never}
|
|
80
99
|
search={params}
|
|
81
100
|
/>
|
|
@@ -103,6 +122,7 @@ export function RouterPager({
|
|
|
103
122
|
) : (
|
|
104
123
|
<Link
|
|
105
124
|
resetScroll={smoothScrollToTop !== true}
|
|
125
|
+
onClick={handleScrollToTop}
|
|
106
126
|
to={location.pathname as never}
|
|
107
127
|
search={params}
|
|
108
128
|
/>
|
|
@@ -130,6 +150,7 @@ export function RouterPager({
|
|
|
130
150
|
) : (
|
|
131
151
|
<Link
|
|
132
152
|
resetScroll={smoothScrollToTop !== true}
|
|
153
|
+
onClick={handleScrollToTop}
|
|
133
154
|
to={location.pathname as never}
|
|
134
155
|
search={params}
|
|
135
156
|
/>
|
|
@@ -156,6 +177,7 @@ export function RouterPager({
|
|
|
156
177
|
) : (
|
|
157
178
|
<Link
|
|
158
179
|
resetScroll={smoothScrollToTop !== true}
|
|
180
|
+
onClick={handleScrollToTop}
|
|
159
181
|
to={location.pathname as never}
|
|
160
182
|
search={params}
|
|
161
183
|
/>
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
10
|
+
|
|
11
|
+
import { scrollWindowToTop } from './scroll-to-top.js'
|
|
12
|
+
|
|
13
|
+
function mockReducedMotion(matches: boolean) {
|
|
14
|
+
vi.stubGlobal(
|
|
15
|
+
'matchMedia',
|
|
16
|
+
vi.fn((query: string) => ({ matches, media: query })) as unknown as typeof window.matchMedia
|
|
17
|
+
)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
vi.unstubAllGlobals()
|
|
22
|
+
vi.restoreAllMocks()
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
describe('scrollWindowToTop', () => {
|
|
26
|
+
it('scrolls to the top of the page', () => {
|
|
27
|
+
const scrollTo = vi.fn()
|
|
28
|
+
vi.stubGlobal('scrollTo', scrollTo)
|
|
29
|
+
mockReducedMotion(false)
|
|
30
|
+
|
|
31
|
+
scrollWindowToTop()
|
|
32
|
+
|
|
33
|
+
expect(scrollTo).toHaveBeenCalledWith({ top: 0, behavior: 'smooth' })
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('skips the animation when the reader asks for reduced motion', () => {
|
|
37
|
+
const scrollTo = vi.fn()
|
|
38
|
+
vi.stubGlobal('scrollTo', scrollTo)
|
|
39
|
+
mockReducedMotion(true)
|
|
40
|
+
|
|
41
|
+
scrollWindowToTop()
|
|
42
|
+
|
|
43
|
+
// Still goes to the top — only the travel is dropped.
|
|
44
|
+
expect(scrollTo).toHaveBeenCalledWith({ top: 0, behavior: 'auto' })
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
/** Older browsers and some test environments have no `matchMedia`. */
|
|
48
|
+
it('still scrolls when matchMedia is unavailable', () => {
|
|
49
|
+
const scrollTo = vi.fn()
|
|
50
|
+
vi.stubGlobal('scrollTo', scrollTo)
|
|
51
|
+
vi.stubGlobal('matchMedia', undefined)
|
|
52
|
+
|
|
53
|
+
expect(() => scrollWindowToTop()).not.toThrow()
|
|
54
|
+
expect(scrollTo).toHaveBeenCalledWith({ top: 0, behavior: 'smooth' })
|
|
55
|
+
})
|
|
56
|
+
})
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Return the reader to the top of the page.
|
|
11
|
+
*
|
|
12
|
+
* Separate from `RouterPager` so the behaviour can be asserted without
|
|
13
|
+
* mounting a router. `smoothScrollToTop` suppresses TanStack's instant
|
|
14
|
+
* `resetScroll` in favour of an animated one, which makes a missing
|
|
15
|
+
* implementation silent — the page simply does not move. That is what
|
|
16
|
+
* happened to every pager carrying the flag.
|
|
17
|
+
*
|
|
18
|
+
* Honours `prefers-reduced-motion`: those readers still arrive at the top,
|
|
19
|
+
* without the travel.
|
|
20
|
+
*/
|
|
21
|
+
export function scrollWindowToTop(): void {
|
|
22
|
+
if (typeof window === 'undefined') return
|
|
23
|
+
|
|
24
|
+
// `matchMedia` is absent in some test and server-ish environments; treating
|
|
25
|
+
// that as "no preference expressed" keeps the scroll working either way.
|
|
26
|
+
const prefersReducedMotion = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches
|
|
27
|
+
window.scrollTo({ top: 0, behavior: prefersReducedMotion === true ? 'auto' : 'smooth' })
|
|
28
|
+
}
|