From 31f2f8eec0fc3c4973b65e950ed166ca973170fb Mon Sep 17 00:00:00 2001 From: dwebxr Date: Thu, 24 Sep 2026 22:23:23 +0900 Subject: [PATCH] =?UTF-8?q?fix(qr):=20QR=20modal=20=E3=81=8C=E8=A6=AA?= =?UTF-8?q?=E3=81=AE=E5=86=8D=E6=8F=8F=E7=94=BB=E3=81=94=E3=81=A8=E3=81=AB?= =?UTF-8?q?=20focus=20=E3=82=92=E5=A5=AA=E3=81=86=E3=83=BB=E9=87=91?= =?UTF-8?q?=E9=A1=8D=E6=AC=84=E3=81=8C=E3=83=A2=E3=83=BC=E3=83=89=E5=88=87?= =?UTF-8?q?=E6=9B=BF=E3=81=A7=E4=BD=9C=E3=82=8A=E7=9B=B4=E3=81=95=E3=82=8C?= =?UTF-8?q?=E3=82=8B=E7=AD=89=E3=82=92=E7=9B=B4=E3=81=99=20(B-R11d)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 第 6 回レビュー R11b で固定した QR 生成画面の既存の挙動 4 件。(1) プレビュー modal の onClose が render ごとに変わり dialog に focus が戻っていた → handler を安定化 (open 時の focus と Escape は従来どおり)。(2) 下部 bar の再描画 effect が初回表示で発火しなかった → 表示状態を依存に加える。(3) 据え置き↔金額指定の切替で金額入力と quick-amount editor が 作り直されていた → 既存 root に hidden を付けて node と入力値を保つ。据え置き → 金額指定の 切替では従来どおり金額欄に focus する (flushSync + ref)。(4) FX の期限切れは注意表示のみの 既存方針を維持 (comment に明記)。支払い URL・EIP-681・receipt の fixture は不変。 Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01HeizmagJBgL5peL5mQpxkc --- components/QrGenerator.tsx | 5 +- components/qr/QrAmountSection.tsx | 127 +++++++------- components/qr/QrPreviewSection.tsx | 12 +- components/qr/QuickAmountEditor.tsx | 4 +- .../QrGenerator.extraction.test.tsx | 157 +++++++++++++++--- tests/components/QrGenerator.test.tsx | 8 +- 6 files changed, 224 insertions(+), 89 deletions(-) diff --git a/components/QrGenerator.tsx b/components/QrGenerator.tsx index 67cb2a10..f4ffcf24 100644 --- a/components/QrGenerator.tsx +++ b/components/QrGenerator.tsx @@ -78,6 +78,8 @@ export function QrGenerator() { const [step2Initialized, setStep2Initialized] = useState(false); // QR は即時表示せず「QRコードを表示する」ボタン → 全画面モーダルで提示。 const [qrModalOpen, setQrModalOpen] = useState(false); + // onClose は modal の focus effect の依存。残高更新や入力で再 focus させない。 + const closeQrModal = useCallback(() => setQrModalOpen(false), []); // 初回に QR モーダルを開いた「ピークモーメント」を latch。以降 A2HS hint を出す // (毎日この QR を使う店主に、ホーム画面への追加を提案する)。閉じても latch は保持。 const [hasOpenedQr, setHasOpenedQr] = useState(false); @@ -231,6 +233,7 @@ export function QrGenerator() { // 旧 QR との互換性を最大化するため token=usdc 時のみ出力する。 crossChain: settings.token === 'usdc' ? crossChainAllowed(settings.chain, settings.crossChain) : undefined, // convert 適用時のみ、期限と顧客への文脈表示 (元価格 + レート) を URL に乗せる。 + // 期限は画面上の目安。期限切れでも QR / exp は保持し、明示的な再計算で更新する。 expiresAt: convert?.expiresAt, priceRefAmount: convert?.anchorAmount, fxRate: convert?.fxRate, @@ -594,7 +597,7 @@ export function QrGenerator() { {payUrl && ( setQrModalOpen(false)} + onClose={closeQrModal} labels={{ title: t('qrModalTitle'), close: t('qrModalClose'), diff --git a/components/qr/QrAmountSection.tsx b/components/qr/QrAmountSection.tsx index e738ffd8..0d9c8d76 100644 --- a/components/qr/QrAmountSection.tsx +++ b/components/qr/QrAmountSection.tsx @@ -2,10 +2,12 @@ import { useMemo, + useRef, type ComponentProps, type Dispatch, type SetStateAction, } from 'react'; +import { flushSync } from 'react-dom'; import { useTranslations } from 'next-intl'; import { Coins } from 'lucide-react'; import { RecoverFeeNotice } from '../RecoverFeeNotice'; @@ -82,6 +84,7 @@ export function QrAmountSection({ recoverGasMode: GasMode; }) { const t = useTranslations('QrGenerator'); + const amountInputRef = useRef(null); // クイック金額は token (JPYC=円 / USDC=ドル) ごとに独立。エディタ・適用とも // 現在の token のサブリストだけを操作する。 @@ -164,7 +167,13 @@ export function QrAmountSection({ key={m} type="button" onClick={() => { - setMode(m as Mode); + if (m === 'amount' && mode === 'static') { + // iOS のキーボードを開けるよう、表示を反映してからタップ中に focus する。 + flushSync(() => setMode('amount')); + amountInputRef.current?.focus(); + } else { + setMode(m); + } resetConvert(); }} className={`flex-1 rounded-md px-3 py-1.5 text-sm font-medium transition ${ @@ -177,59 +186,60 @@ export function QrAmountSection({ ))} - {mode === 'amount' ? ( -
- {/* 金額ヒーロー: 入力欄を「表示器」化して数字を主役に。通貨記号は控えめな - 接尾、その真下に参考円 (USDC のみ・JPYC は ¥ ペッグで冗長ゆえ非表示)。 - 枠線は container 側に寄せ、focus で brand + 浮き影。 */} -
-
- { - setAmount( - truncateAmount(e.target.value, deployment.decimals), - ); - resetConvert(); - }} - placeholder={settings.token === 'jpyc' ? '1000' : '10.00'} - aria-label={t('amountLabel', { - symbol: deployment.displaySymbol, - })} - className="min-w-0 flex-1 bg-transparent text-right text-4xl font-bold tabular-nums tracking-tight text-slate-900 placeholder:text-slate-300 focus:outline-none sm:text-5xl" - autoFocus - /> - - {deployment.displaySymbol} - -
- {fiatHint && ( -
- {fiatHint} -
- )} + {/* 非表示でも node を保持し、再表示で入力欄を作り直さない。 */} +