@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.
@@ -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.17.0",
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/analytics": "4.17.0",
129
- "@byline/ai": "4.17.0",
130
- "@byline/auth": "4.17.0",
131
- "@byline/analytics-agent": "4.17.0",
132
- "@byline/i18n": "4.17.0",
133
- "@byline/admin": "4.17.0",
134
- "@byline/core": "4.17.0",
135
- "@byline/ui": "4.17.0",
136
- "@byline/client": "4.17.0"
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
+ }