@aiquants/daily-report 0.7.0 → 0.9.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/README.md +13 -6
- package/dist/client.d.mts +438 -26
- package/dist/client.d.ts +438 -26
- package/dist/client.js +5 -4
- package/dist/client.js.map +1 -1
- package/dist/client.mjs +5 -4
- package/dist/client.mjs.map +1 -1
- package/dist/index.d.mts +115 -3
- package/dist/index.d.ts +115 -3
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/server.d.mts +15 -0
- package/dist/server.d.ts +15 -0
- package/dist/server.js +9 -6
- package/dist/server.js.map +1 -1
- package/dist/server.mjs +9 -6
- package/dist/server.mjs.map +1 -1
- package/dist/{sse-schema-Df49KA7B.d.mts → sse-schema-BiUnufrI.d.mts} +12 -0
- package/dist/{sse-schema-RHckD7TS.d.ts → sse-schema-YZh7Pz7i.d.ts} +12 -0
- package/dist/styles/daily-report.standalone.css +1 -1
- package/package.json +3 -3
- package/src/client/components/daily-report-comment-item.tsx +6 -1
- package/src/client/components/daily-report-detail-list.tsx +65 -34
- package/src/client/components/daily-report-edit-form.tsx +6 -0
- package/src/client/components/daily-report-ids-stream-status.spec.tsx +93 -0
- package/src/client/components/daily-report-ids-stream-status.tsx +67 -0
- package/src/client/components/daily-report-list.tsx +88 -55
- package/src/client/components/daily-report-page.spec.tsx +119 -0
- package/src/client/components/daily-report-page.tsx +62 -69
- package/src/client/components/daily-report-resolved-content.tsx +36 -10
- package/src/client/components/report-views.spec.tsx +37 -2
- package/src/client/config-context.tsx +18 -0
- package/src/client/contexts/daily-report-action-context.spec.tsx +278 -0
- package/src/client/contexts/daily-report-action-context.tsx +203 -65
- package/src/client/contexts/daily-report-error-context.spec.tsx +66 -0
- package/src/client/contexts/daily-report-error-context.tsx +98 -0
- package/src/client/hooks/use-animated-number.spec.ts +103 -0
- package/src/client/hooks/use-animated-number.ts +69 -0
- package/src/client/hooks/use-daily-report.spec.ts +50 -2
- package/src/client/hooks/use-daily-report.ts +30 -1
- package/src/client/route-helpers.spec.ts +100 -0
- package/src/client/route-helpers.ts +20 -41
- package/src/client/streaming/daily-report-ids-stream-client.spec.ts +704 -0
- package/src/client/streaming/daily-report-ids-stream-client.ts +677 -0
- package/src/client/streaming/daily-report-ids-stream-session.spec.ts +360 -0
- package/src/client/streaming/daily-report-ids-stream-session.ts +280 -0
- package/src/client.ts +5 -0
- package/src/index.ts +1 -0
- package/src/server/handlers.action.spec.ts +44 -0
- package/src/server/handlers.ids-stream.spec.ts +414 -0
- package/src/server/handlers.ts +324 -8
- package/src/server/service.spec.ts +83 -7
- package/src/server/service.ts +100 -3
- package/src/server/test-helpers/handlers-config.ts +48 -1
- package/src/server.spec.ts +49 -0
- package/src/server.ts +7 -0
- package/src/shared/business-date.spec.ts +22 -1
- package/src/shared/business-date.ts +20 -0
- package/src/shared/comment-adapter.spec.ts +38 -0
- package/src/shared/comment-adapter.ts +9 -7
- package/src/shared/ids-stream.spec.ts +65 -0
- package/src/shared/ids-stream.ts +139 -0
- package/src/shared/sse-schema.ts +5 -0
- package/src/shared/text-utils.spec.ts +42 -0
- package/src/shared/text-utils.ts +8 -0
|
@@ -1,54 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Top-level daily-report page content (chrome-agnostic).
|
|
3
|
-
*
|
|
2
|
+
* Top-level daily-report page content (chrome-agnostic, stream-driven).
|
|
3
|
+
* アプリのレイアウトに依存しない日報ページ本体コンポーネント (ストリーム駆動)。
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* ID 一覧はモジュール常駐の NDJSON ストリームセッションから購読し、最初の
|
|
6
|
+
* チャンク到着と同時に一覧を描画する (全件解決を待つ Await ゲートは廃止)。
|
|
7
|
+
* 再訪時はセッションに残る受信済みデータで即座に復元し、鮮度はバックグラウンド
|
|
8
|
+
* 再検証 (SWR) と SSE が担う。アプリ側のルートは本コンポーネントを自前の
|
|
9
|
+
* レイアウトで包み、`config` でヘッダー描画・パス・ラベル名を注入する。
|
|
7
10
|
*/
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import type { DailyReportItem, DailyReportUser } from "../../shared/types"
|
|
11
|
+
import { useEffect } from "react"
|
|
12
|
+
import type { DailyReportUser } from "../../shared/types"
|
|
11
13
|
import { type DailyReportClientConfigInput, DailyReportConfigProvider, useDailyReportConfig } from "../config-context"
|
|
12
14
|
import { DailyReportActionProvider } from "../contexts/daily-report-action-context"
|
|
15
|
+
import { DailyReportErrorProvider } from "../contexts/daily-report-error-context"
|
|
16
|
+
import { ensureDailyReportIdsStreamSession, reloadDocument, retryDailyReportIdsStream, useDailyReportIdsStream } from "../streaming/daily-report-ids-stream-session"
|
|
13
17
|
import { DAILY_REPORT_FETCH_DEBOUNCE_MS } from "../utils/constants"
|
|
18
|
+
import { DailyReportIdsStreamStatus } from "./daily-report-ids-stream-status"
|
|
14
19
|
import { DailyReportResolvedContent } from "./daily-report-resolved-content"
|
|
15
20
|
|
|
16
21
|
/**
|
|
17
|
-
*
|
|
18
|
-
*
|
|
22
|
+
* Error display when the ids stream terminally failed before any data arrived.
|
|
23
|
+
* データ未受信のまま ID ストリームが力尽きたときのエラー表示コンポーネント。
|
|
19
24
|
*
|
|
20
|
-
*
|
|
21
|
-
*/
|
|
22
|
-
const DailyReportResolvedSync = ({ dailyReportItems, userId, onResolved }: { dailyReportItems: DailyReportItem[]; userId?: string | null; onResolved: (items: DailyReportItem[]) => void }) => {
|
|
23
|
-
/**
|
|
24
|
-
* Notifies parent when resolved items change.
|
|
25
|
-
* Dependencies:
|
|
26
|
-
* - dailyReportItems: latest resolved list from Await.
|
|
27
|
-
* - onResolved: parent synchronization callback.
|
|
28
|
-
* Cleanup:
|
|
29
|
-
* - None (pure synchronization side effect).
|
|
30
|
-
*
|
|
31
|
-
* 解決済み items が変化したときに親へ通知します。
|
|
32
|
-
* 依存配列:
|
|
33
|
-
* - dailyReportItems: Await で解決された最新リスト
|
|
34
|
-
* - onResolved: 親への同期コールバック
|
|
35
|
-
* クリーンアップ:
|
|
36
|
-
* - なし(同期のみの副作用)
|
|
37
|
-
*/
|
|
38
|
-
useEffect(() => {
|
|
39
|
-
onResolved(dailyReportItems)
|
|
40
|
-
}, [dailyReportItems, onResolved])
|
|
41
|
-
|
|
42
|
-
return <DailyReportResolvedContent dailyReportItems={dailyReportItems} userId={userId} />
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Error display component for daily report data fetch failures.
|
|
47
|
-
* 日報データの取得失敗時に表示するエラーコンポーネント。
|
|
25
|
+
* 再試行はストリームセッションの retry へ直結する (受信済みカーソルから再開)。
|
|
48
26
|
*/
|
|
49
27
|
export const DailyReportLoadError = () => {
|
|
50
28
|
const { title, renderHeader } = useDailyReportConfig()
|
|
51
|
-
const { revalidate } = useRevalidator()
|
|
52
29
|
return (
|
|
53
30
|
<>
|
|
54
31
|
{renderHeader({ title, annotation: <span className="font-bold text-red-500">Error</span> })}
|
|
@@ -56,7 +33,11 @@ export const DailyReportLoadError = () => {
|
|
|
56
33
|
<div className="mx-auto flex h-full w-full max-w-6xl flex-1 flex-col gap-6">
|
|
57
34
|
<div className="flex h-full flex-1 flex-col items-center justify-center gap-4 rounded-lg border border-red-200 bg-white p-4 text-slate-500 shadow-sm dark:border-red-900 dark:bg-slate-900/70 dark:text-slate-400">
|
|
58
35
|
<p className="font-medium text-red-600">日報データの読み込みに失敗しました。</p>
|
|
59
|
-
<button
|
|
36
|
+
<button
|
|
37
|
+
type="button"
|
|
38
|
+
data-testid="daily-report-load-error-retry"
|
|
39
|
+
onClick={() => retryDailyReportIdsStream()}
|
|
40
|
+
className="rounded bg-slate-100 px-4 py-2 text-slate-700 text-sm hover:bg-slate-200 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-slate-700">
|
|
60
41
|
再読み込み
|
|
61
42
|
</button>
|
|
62
43
|
</div>
|
|
@@ -67,8 +48,8 @@ export const DailyReportLoadError = () => {
|
|
|
67
48
|
}
|
|
68
49
|
|
|
69
50
|
/**
|
|
70
|
-
*
|
|
71
|
-
*
|
|
51
|
+
* Loading screen shown only until the first stream chunk arrives.
|
|
52
|
+
* 最初のストリームチャンクが届くまでの間だけ表示するローディング画面。
|
|
72
53
|
*/
|
|
73
54
|
export const DailyReportLoading = () => {
|
|
74
55
|
const { title, renderHeader } = useDailyReportConfig()
|
|
@@ -77,7 +58,8 @@ export const DailyReportLoading = () => {
|
|
|
77
58
|
{renderHeader({
|
|
78
59
|
title,
|
|
79
60
|
annotation: (
|
|
80
|
-
<div className="flex flex-wrap gap-2 leading-none">
|
|
61
|
+
<div className="flex flex-wrap items-center gap-2 leading-none">
|
|
62
|
+
<DailyReportIdsStreamStatus />
|
|
81
63
|
<span>総件数: 読み込み中...</span>
|
|
82
64
|
<span>デバウンス: {DAILY_REPORT_FETCH_DEBOUNCE_MS} ms</span>
|
|
83
65
|
</div>
|
|
@@ -97,42 +79,53 @@ export type DailyReportPageProps = {
|
|
|
97
79
|
user?: DailyReportUser
|
|
98
80
|
/** 難読化済みの内部ユーザー ID (未登録ユーザーは null)。 */
|
|
99
81
|
userId?: string | null
|
|
100
|
-
/** clientLoader で defer した日報 ID 一覧 (Promise) または解決済み配列。 */
|
|
101
|
-
dailyReportIds?: Promise<DailyReportItem[]> | DailyReportItem[] | null
|
|
102
82
|
/** クライアント設定の部分上書き (ヘッダー描画・パス・ラベル名・フィールドラベルなど)。 */
|
|
103
83
|
config?: DailyReportClientConfigInput
|
|
104
84
|
}
|
|
105
85
|
|
|
106
86
|
/**
|
|
107
|
-
*
|
|
108
|
-
*
|
|
87
|
+
* Stream-driven page body: session bootstrap, phase gating, and provider wiring.
|
|
88
|
+
* ストリーム駆動のページ本体。セッション確立・局面ゲーティング・プロバイダー結線を担う。
|
|
109
89
|
*/
|
|
110
|
-
const DailyReportPageInner = ({ user, userId
|
|
111
|
-
const
|
|
90
|
+
const DailyReportPageInner = ({ user, userId }: Omit<DailyReportPageProps, "config">) => {
|
|
91
|
+
const { apiBasePath } = useDailyReportConfig()
|
|
92
|
+
const stream = useDailyReportIdsStream()
|
|
112
93
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
94
|
+
/**
|
|
95
|
+
* ストリームセッション確立の保険。
|
|
96
|
+
* 通常は clientLoader が先に確立するが、clientLoader を経ないマウントや
|
|
97
|
+
* ユーザー切替 (userKey 不一致でセッション再生成) をここで拾う。
|
|
98
|
+
* 依存配列: apiBasePath / userId (いずれかの変化で再確認)。
|
|
99
|
+
* クリーンアップ: なし (セッションはモジュール常駐で、アンマウント後も生存させる)。
|
|
100
|
+
*/
|
|
101
|
+
useEffect(() => {
|
|
102
|
+
ensureDailyReportIdsStreamSession({ apiBasePath, userKey: userId ?? null })
|
|
103
|
+
}, [apiBasePath, userId])
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* 認証切れ (ストリームが 401/403) はドキュメントのフルリロードへ倒し、
|
|
107
|
+
* サーバー loader のログインリダイレクトに委譲する。
|
|
108
|
+
* 依存配列: stream.phase。クリーンアップ: なし。
|
|
109
|
+
*/
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
if (stream.phase === "auth-required") {
|
|
112
|
+
reloadDocument()
|
|
113
|
+
}
|
|
114
|
+
}, [stream.phase])
|
|
116
115
|
|
|
116
|
+
// 1 件でも届いたら (または空リストで完走したら) 本体を描画する
|
|
117
|
+
const showContent = stream.items.length > 0 || stream.phase === "complete"
|
|
117
118
|
return (
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
return <DailyReportResolvedSync dailyReportItems={resolvedItems as DailyReportItem[]} userId={userId} onResolved={handleResolvedItems} />
|
|
129
|
-
}}
|
|
130
|
-
</Await>
|
|
131
|
-
) : (
|
|
132
|
-
<DailyReportLoading />
|
|
133
|
-
)}
|
|
134
|
-
</Suspense>
|
|
135
|
-
</DailyReportActionProvider>
|
|
119
|
+
// key でユーザーごとにプロバイダーをリマウントする。リロード無しのユーザー切替
|
|
120
|
+
// (再ログイン・成り代わり) で items / removedIds / 保留キュー等の状態が旧ユーザーの
|
|
121
|
+
// まま残ると、render-phase マージが旧ユーザーの行を新ユーザーの一覧へ持ち越す
|
|
122
|
+
// エラーサーフェスはユーザー切替のリマウント境界より外側 (バナーは切替を跨いで消えてよい)。
|
|
123
|
+
// アクションプロバイダーはこの notifyError を購読して失敗を可視化する
|
|
124
|
+
<DailyReportErrorProvider>
|
|
125
|
+
<DailyReportActionProvider key={userId ?? "anonymous"} user={user} initialItems={stream.items} userId={userId}>
|
|
126
|
+
{showContent ? <DailyReportResolvedContent userId={userId} /> : stream.phase === "failed" ? <DailyReportLoadError /> : <DailyReportLoading />}
|
|
127
|
+
</DailyReportActionProvider>
|
|
128
|
+
</DailyReportErrorProvider>
|
|
136
129
|
)
|
|
137
130
|
}
|
|
138
131
|
|
|
@@ -2,35 +2,41 @@ import { Plus, RefreshCw, RotateCw } from "lucide-react"
|
|
|
2
2
|
import type { ReactNode } from "react"
|
|
3
3
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
|
|
4
4
|
import { useSearchParams } from "react-router"
|
|
5
|
-
import
|
|
5
|
+
import { resolveTodayBusinessDateKey } from "../../shared/business-date"
|
|
6
6
|
import { useDailyReportConfig } from "../config-context"
|
|
7
7
|
import { useDailyReportActionContext } from "../contexts/daily-report-action-context"
|
|
8
|
+
import { useAnimatedNumber } from "../hooks/use-animated-number"
|
|
8
9
|
import { Button } from "../ui/button"
|
|
9
10
|
import { Label } from "../ui/label"
|
|
10
11
|
import { Switch } from "../ui/switch"
|
|
11
12
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs"
|
|
12
13
|
import { DAILY_REPORT_FETCH_DEBOUNCE_MS } from "../utils/constants"
|
|
13
14
|
import { DailyReportDetailList } from "./daily-report-detail-list"
|
|
15
|
+
import { DailyReportIdsStreamStatus } from "./daily-report-ids-stream-status"
|
|
14
16
|
import { DailyReportList } from "./daily-report-list"
|
|
15
17
|
|
|
16
18
|
const tabTriggerClass = "min-w-24 p-2 font-medium text-slate-600 text-xs transition-colors data-[state=active]:bg-white data-[state=active]:text-slate-900 sm:min-w-36 sm:text-sm dark:text-slate-300 dark:data-[state=active]:bg-slate-800 dark:data-[state=active]:text-slate-100"
|
|
17
19
|
|
|
18
20
|
type DailyReportResolvedContentProps = {
|
|
19
|
-
dailyReportItems: DailyReportItem[]
|
|
20
21
|
userId?: string | null
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
/**
|
|
24
|
-
* Displays
|
|
25
|
-
*
|
|
25
|
+
* Displays streamed daily reports with layout and tabbed virtual scroll views.
|
|
26
|
+
* レイアウトとタブ付き仮想スクロールでストリーム受信済み日報を表示するコンポーネント。
|
|
27
|
+
*
|
|
28
|
+
* アイテムは Action Context (ストリームチャンクをマージ済み) から読む。
|
|
26
29
|
*/
|
|
27
|
-
export const DailyReportResolvedContent = ({
|
|
30
|
+
export const DailyReportResolvedContent = ({ userId }: DailyReportResolvedContentProps) => {
|
|
28
31
|
// 自動既読設定
|
|
29
32
|
const [autoMarkRead, setAutoMarkRead] = useState(false)
|
|
30
33
|
const { title, renderHeader, showDevControls } = useDailyReportConfig()
|
|
31
34
|
const { isSseEnabled, toggleSse, createReport, items: dailyReportItems, refetchData, clearCacheAndRefetch } = useDailyReportActionContext()
|
|
32
35
|
const [searchParams] = useSearchParams()
|
|
33
36
|
|
|
37
|
+
// 総件数の表示値 (チャンク到着ごとの跳びをイーズアウトで滑らかにする)
|
|
38
|
+
const animatedItemCount = useAnimatedNumber(dailyReportItems.length)
|
|
39
|
+
|
|
34
40
|
// 各タブのスクロール位置をアンマウント後も保持する Ref
|
|
35
41
|
const listScrollOffsetRef = useRef<number>(0)
|
|
36
42
|
const detailListScrollOffsetRef = useRef<number>(0)
|
|
@@ -49,6 +55,24 @@ export const DailyReportResolvedContent = ({ dailyReportItems: _initialItems, us
|
|
|
49
55
|
return dailyReportItems[0].reportHubId
|
|
50
56
|
})
|
|
51
57
|
|
|
58
|
+
// 初回の自動選択を一度きりにするフラグ (ユーザーの明示的な選択解除を上書きしない)
|
|
59
|
+
const hasAutoSelectedRef = useRef(false)
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 最初のアイテム到着時に先頭日報を一度だけ自動選択する副作用。
|
|
63
|
+
* useState 初期化子はマウント時点で context の items がまだ空のため実質機能せず、
|
|
64
|
+
* ストリーミングでは最初のチャンク到着後に選択を確定させる必要がある。
|
|
65
|
+
* モバイルのオーバーレイは isMobileDetailOpen (明示タップ) でのみ開くため、
|
|
66
|
+
* プログラム選択でオーバーレイが開くことはない。
|
|
67
|
+
* 依存配列: dailyReportItems (チャンク到着ごと)。クリーンアップ: なし。
|
|
68
|
+
*/
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
if (!hasAutoSelectedRef.current && dailyReportItems.length > 0) {
|
|
71
|
+
hasAutoSelectedRef.current = true
|
|
72
|
+
setSelectedItemId((prev) => prev ?? dailyReportItems[0].reportHubId)
|
|
73
|
+
}
|
|
74
|
+
}, [dailyReportItems])
|
|
75
|
+
|
|
52
76
|
const handleReload = async () => {
|
|
53
77
|
await refetchData()
|
|
54
78
|
}
|
|
@@ -59,8 +83,9 @@ export const DailyReportResolvedContent = ({ dailyReportItems: _initialItems, us
|
|
|
59
83
|
|
|
60
84
|
const handleCreateReport = async () => {
|
|
61
85
|
const businessDateParam = searchParams.get("businessDate")
|
|
62
|
-
|
|
63
|
-
|
|
86
|
+
// 「本日」はローカル暦日で決める。toISOString() の UTC 日付だと JST 深夜〜午前帯に
|
|
87
|
+
// 前日の営業日へ新規日報が登録されてしまう
|
|
88
|
+
const dateToUse = businessDateParam || resolveTodayBusinessDateKey()
|
|
64
89
|
const newId = await createReport(dateToUse)
|
|
65
90
|
setSelectedItemId(newId)
|
|
66
91
|
}
|
|
@@ -123,13 +148,14 @@ export const DailyReportResolvedContent = ({ dailyReportItems: _initialItems, us
|
|
|
123
148
|
{renderHeader({
|
|
124
149
|
title,
|
|
125
150
|
annotation: (
|
|
126
|
-
<div className="flex flex-wrap gap-2 leading-1">
|
|
127
|
-
<
|
|
151
|
+
<div className="flex flex-wrap items-center gap-2 leading-1">
|
|
152
|
+
<DailyReportIdsStreamStatus />
|
|
153
|
+
<span className="tabular-nums">総件数: {animatedItemCount.toLocaleString()} 件</span>
|
|
128
154
|
<span>デバウンス: {DAILY_REPORT_FETCH_DEBOUNCE_MS} ms</span>
|
|
129
155
|
</div>
|
|
130
156
|
),
|
|
131
157
|
})}
|
|
132
|
-
<div className="h-full bg-slate-50 dark:bg-slate-
|
|
158
|
+
<div className="h-full bg-slate-50 dark:bg-slate-950">
|
|
133
159
|
<div className="flex h-full w-full max-w-6xl flex-1 flex-col p-1">
|
|
134
160
|
<Tabs defaultValue="list" className="flex h-full flex-1 flex-col">
|
|
135
161
|
<div className="flex items-center justify-between gap-2 rounded-md bg-slate-50 p-1 transition-colors dark:bg-slate-900/70 dark:text-slate-200">
|
|
@@ -39,7 +39,7 @@ import { CONTENT_DRAG_POINTER_INPUTS, ITEM_VERTICAL_PADDING, TAP_SCROLL_CIRCLE_O
|
|
|
39
39
|
type CapturedVirtualScrollProps = {
|
|
40
40
|
testId?: string
|
|
41
41
|
itemCount: number
|
|
42
|
-
behaviorOptions?: { pointerDragInputs?: unknown; enablePointerDrag?: unknown }
|
|
42
|
+
behaviorOptions?: { pointerDragInputs?: unknown; enablePointerDrag?: unknown; enableKeyboardNavigation?: unknown }
|
|
43
43
|
scrollBarOptions?: { tapScrollCircleOptions?: unknown; renderThumbOverlay?: (props: Record<string, unknown>) => ReactNode }
|
|
44
44
|
getItemKey?: unknown
|
|
45
45
|
getItemHeight?: unknown
|
|
@@ -48,6 +48,7 @@ type CapturedVirtualScrollProps = {
|
|
|
48
48
|
onRangeChange?: unknown
|
|
49
49
|
onScroll?: unknown
|
|
50
50
|
contentInsets?: unknown
|
|
51
|
+
contentProps?: Record<string, unknown>
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
const captured: CapturedVirtualScrollProps[] = []
|
|
@@ -84,10 +85,13 @@ vi.mock("@aiquants/virtualscroll", async () => {
|
|
|
84
85
|
onRangeChange: props.onRangeChange,
|
|
85
86
|
onScroll: props.onScroll,
|
|
86
87
|
contentInsets: props.contentInsets,
|
|
88
|
+
contentProps: props.contentProps,
|
|
87
89
|
})
|
|
88
90
|
useHandle(ref, () => ({ scrollTo: () => 0, scrollToIndex: () => {}, getScrollPosition: () => 0, getContentSize: () => 0, getViewportSize: () => 0, updateItemSize: () => {}, focusItemAtIndex: () => {} }))
|
|
91
|
+
// 本物は contentProps をコンテンツ要素 (行だけを所有する list) へ透過する。
|
|
92
|
+
// ロール階層 (list → listitem) を DOM で検査できるよう代役でも同じ口へ流す
|
|
89
93
|
return (
|
|
90
|
-
<div data-testid={props.testId}>
|
|
94
|
+
<div data-testid={props.testId} {...props.contentProps}>
|
|
91
95
|
{Array.from({ length: props.itemCount }, (_, index) => (
|
|
92
96
|
// biome-ignore lint/suspicious/noArrayIndexKey: 代役なので index キーで十分
|
|
93
97
|
<div key={index}>{props.children(props.getItem(index), index)}</div>
|
|
@@ -338,3 +342,34 @@ describe("日報ビューの描画と結線", () => {
|
|
|
338
342
|
expect(detailViews).toEqual(REPORT_DETAIL_VIEWS.map((view) => view.file).sort())
|
|
339
343
|
})
|
|
340
344
|
})
|
|
345
|
+
|
|
346
|
+
describe("日報ビューのアクセシビリティ (仮想化リストのセマンティクス)", () => {
|
|
347
|
+
it.each(VIEW_CASES)("%s は行だけを所有する list をコンテンツ要素へ置く", (_file, view) => {
|
|
348
|
+
// role="list" を「スクロールバーと兄弟の」コンテンツ要素へ置く (contentProps 経由)。
|
|
349
|
+
// ペイン以上の階層に置くと ScrollBar の role="scrollbar" が不許可の所有子になる
|
|
350
|
+
renderView(view.render())
|
|
351
|
+
const props = optionsOf(view.testId)
|
|
352
|
+
expect(props.contentProps?.role).toBe("list")
|
|
353
|
+
expect(typeof props.contentProps?.["aria-label"]).toBe("string")
|
|
354
|
+
expect((props.contentProps?.["aria-label"] as string).length).toBeGreaterThan(0)
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
it.each(VIEW_CASES)("%s は enableKeyboardNavigation を false にする (タブ順汚染と focus 奪取の防止)", (_file, view) => {
|
|
358
|
+
// 既定 true は行ラッパーへ tabIndex=-1 を付けてタブ順を汚し、pointerdown で
|
|
359
|
+
// 行へフォーカスを移す (★/既読タップで詳細ペインが黙って切り替わる不具合の原因)。
|
|
360
|
+
// false にすることで list/listitem のロール階層も有効になる
|
|
361
|
+
renderView(view.render())
|
|
362
|
+
expect(optionsOf(view.testId).behaviorOptions?.enableKeyboardNavigation).toBe(false)
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
it.each(VIEW_CASES)("%s は各行へ role=listitem と aria-posinset/aria-setsize を付ける", (_file, view) => {
|
|
366
|
+
// 仮想化で描画されるのは窓分だけなので、絶対位置 (posinset) と全件数 (setsize) を
|
|
367
|
+
// 各行に載せないと SR は「全 N 件中 M 番目」を読めない
|
|
368
|
+
const { container } = renderView(view.render())
|
|
369
|
+
const listItems = container.querySelectorAll('[role="listitem"]')
|
|
370
|
+
expect(listItems.length).toBe(1)
|
|
371
|
+
const row = listItems[0]
|
|
372
|
+
expect(row.getAttribute("aria-posinset")).toBe("1")
|
|
373
|
+
expect(row.getAttribute("aria-setsize")).toBe("1")
|
|
374
|
+
})
|
|
375
|
+
})
|
|
@@ -43,6 +43,19 @@ export type DailyReportFieldLabels = {
|
|
|
43
43
|
relationsEmpty: string
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Mutation-failure notification payload (for a host-supplied error surface).
|
|
48
|
+
* ミューテーション失敗の通知情報 (ホストが独自の通知 UI を出すための口)。
|
|
49
|
+
*/
|
|
50
|
+
export type DailyReportErrorInfo = {
|
|
51
|
+
/** 失敗した操作の種別 (create / update / publish / delete / addComment / deleteComment / toggleStar / toggleRead)。 */
|
|
52
|
+
operation: string
|
|
53
|
+
/** 表示用のメッセージ (パッケージ内蔵バナー用)。 */
|
|
54
|
+
message: string
|
|
55
|
+
/** 元例外 (診断用)。 */
|
|
56
|
+
cause?: unknown
|
|
57
|
+
}
|
|
58
|
+
|
|
46
59
|
/** データソースごとのバッジ表示設定。 */
|
|
47
60
|
export type SourceTypeConfig = {
|
|
48
61
|
label: string
|
|
@@ -69,6 +82,11 @@ export type DailyReportClientConfig = {
|
|
|
69
82
|
renderHeader: (props: DailyReportHeaderProps) => ReactNode
|
|
70
83
|
/** 開発向けコントロール (SSE 購読トグル等) を表示するか。 */
|
|
71
84
|
showDevControls: boolean
|
|
85
|
+
/**
|
|
86
|
+
* ミューテーション失敗時の通知ポート。未指定ならパッケージ内蔵の role="alert"
|
|
87
|
+
* バナーで表示する。ホストが独自トースト等を持つ場合はここへ委譲する。
|
|
88
|
+
*/
|
|
89
|
+
onError?: (info: DailyReportErrorInfo) => void
|
|
72
90
|
}
|
|
73
91
|
|
|
74
92
|
/**
|