• #vue
  • #nuxt
  • #debugging
  • #bug-fix
未分類

コーディング規約ビューアページのバグ分析と修正計画

報告されたバグ

1. キーボードショートカットが動作しない

  • 症状: Ctrl+] (次へ) と Ctrl+[ (前へ) のキーボードショートカットが動作しない
  • 影響範囲: /coding-standards/viewer#rule-2-1 などの全体ビューページ

2. スクロールができない

  • 症状: ページのスクロールが正常に動作しない
  • 影響範囲: 全体ビューページ

コード分析結果

ファイル: apps/web/app/pages/coding-standards/viewer/index.vue

キーボードショートカットの実装状況

定義箇所 (Line 248-256):

function handleKeydown(e) {
  if (e.ctrlKey && e.key === '[') {
    e.preventDefault();
    goToPrevStep();
  } else if (e.ctrlKey && e.key === ']') {
    e.preventDefault();
    goToNextStep();
  }
}

イベントリスナー登録 (Line 370):

window.addEventListener('keydown', handleKeydown);

イベントリスナー解除 (Line 402):

window.removeEventListener('keydown', handleKeydown);

実装状態: ✅ コード上は正しく実装されている

スクロール機能の実装状況

スクロール可能な高さの設定 (Line 555):

.step {
  min-height: 150vh;
  position: relative;
}

右側コードエリアの固定 (Line 595-603):

.viz {
  position: sticky;
  top: 8vh;
  height: 84vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
}

実装状態: ✅ コード上は正しく実装されている

問題の原因仮説

1. ClientOnlyコンポーネントの影響

Line 2-90: 全体が<ClientOnly>でラップされている

<template>
  <ClientOnly>
    <div class="breadcrumb-nav">
      ...
    </div>
    ...
  </ClientOnly>
</template>

仮説:

  • ClientOnlyコンポーネントによるレンダリングのタイミングの問題
  • SSR(サーバーサイドレンダリング)とCSR(クライアントサイドレンダリング)の切り替え時にイベントリスナーが正しく登録されない可能性
  • ハイドレーションミスマッチが発生している可能性

2. z-indexの定義不足

Line 567-577: 11個のステップまでしかz-indexが定義されていない

.step[data-step="0"] .step-content { z-index: 1; }
.step[data-step="1"] .step-content { z-index: 2; }
...
.step[data-step="10"] .step-content { z-index: 11; }

問題点:

  • JSONデータには100以上のルールが存在する
  • 12番目以降のステップにはz-indexが設定されない
  • ステップ要素が重なって表示される可能性がある
  • これによりクリックイベントやフォーカスが正しく機能しない可能性

3. イベントリスナーの競合

可能性のある競合:

  • IntersectionObserver (Line 349-364)
  • Scroll イベント (Line 368)
  • Keydown イベント (Line 370)

これらのイベントが同時に動作することで、競合が発生している可能性がある。

4. ハッシュナビゲーションの副作用

Line 373-392: ハッシュによるナビゲーション処理

if (window.location.hash) {
  const hash = window.location.hash.slice(1);
  const targetElement = document.getElementById(hash);
  if (targetElement) {
    // ...スクロール処理
  }
}

問題の可能性:

  • ハッシュナビゲーション後にフォーカスが失われる
  • キーボードイベントが要素ではなくwindowに登録されているため、フォーカスの影響を受けるべきではないが、何らかの副作用がある可能性

修正計画

Phase 1: 緊急修正(即座に実施)

1-1. z-index問題の解決

方法A: 動的z-index生成

<style>
/* 静的なz-index定義を削除 */
</style>

<script setup>
// 動的にz-indexを設定
const getStepZIndex = (index) => index + 1;
</script>

<template>
<div class="step-content" :style="{ zIndex: getStepZIndex(index) }">
  ...
</div>
</template>

方法B: CSSカスタムプロパティを使用

<template>
<div class="step-content" :style="{ '--step-index': index }">
  ...
</div>
</template>

<style>
.step-content {
  z-index: calc(var(--step-index) + 1);
}
</style>

推奨: 方法B(CSSカスタムプロパティ)- よりパフォーマンスが良い

1-2. イベントリスナーの確実な登録

onMounted(async () => {
  await loadSteps();

  // 他の初期化処理...

  await nextTick();

  // イベントリスナーを最後に登録(確実に要素がレンダリングされた後)
  await nextTick(); // 2回目のnextTickで確実に
  window.addEventListener('keydown', handleKeydown);

  console.log('Keydown listener registered'); // デバッグ用
});

Phase 2: 根本原因の調査(Chrome DevToolsで確認)

2-1. イベントリスナーの確認

Chrome DevToolsで以下を確認:

  1. Elements > Event Listeners タブでkeydownイベントが登録されているか
  2. Console で以下を実行:
    getEventListeners(window).keydown
    

2-2. DOM要素の状態確認

  1. .step要素のz-indexが正しく適用されているか
  2. スクロール可能な高さが十分にあるか
  3. overflowプロパティが正しく設定されているか

2-3. ハイドレーションエラーの確認

Console で以下のエラーをチェック:

  • "Hydration node mismatch"
  • "Hydration completed but contains mismatches"

Phase 3: 構造的な改善(長期対応)

3-1. ClientOnlyの削除検討

<template>
  <!-- ClientOnlyを削除 -->
  <div class="breadcrumb-nav">
    ...
  </div>
  ...
</template>

<script setup>
// onMountedの処理を調整
onMounted(() => {
  // ブラウザ環境でのみ実行される処理
  if (process.client) {
    // ...
  }
});
</script>

3-2. スクロール処理の最適化

// Passive scrollイベントの活用
window.addEventListener('scroll', updateProgress, {
  passive: true,
  capture: false
});

// IntersectionObserverのオプション最適化
io = new IntersectionObserver(
  (entries) => {
    // ...
  },
  {
    root: null,
    threshold: [0, 0.25, 0.5, 0.75, 1], // より細かいしきい値
    rootMargin: '0px'
  }
);

3-3. Composableへの分離

// composables/useCodingStandardsViewer.ts
export function useCodingStandardsViewer() {
  const steps = ref([]);
  const activeIndex = ref(0);

  function handleKeydown(e: KeyboardEvent) {
    // ...
  }

  onMounted(() => {
    window.addEventListener('keydown', handleKeydown);
  });

  onBeforeUnmount(() => {
    window.removeEventListener('keydown', handleKeydown);
  });

  return {
    steps,
    activeIndex,
    // ...
  };
}

実装優先順位

最優先(今すぐ実施)

  1. z-indexの動的生成 - 方法Bを採用
  2. イベントリスナー登録の確実化 - nextTick を2回使用
  3. デバッグログの追加 - 問題箇所の特定

次に実施(動作確認後)

  1. Chrome DevToolsでの詳細調査 - イベントリスナー、DOM状態、エラーの確認
  2. スクロール処理の最適化 - passiveオプション、IntersectionObserverの調整

長期的に検討

  1. ClientOnlyの削除 - SSR/CSRの切り替えを適切に処理
  2. Composableへの分離 - コードの可読性と保守性向上

期待される結果

修正後の動作

  1. キーボードショートカット:
    • Ctrl+]: 次のルールにスムーズに移動
    • Ctrl+[: 前のルールにスムーズに移動
  2. スクロール:
    • マウスホイールでのスクロールが正常に動作
    • タッチパッドでのスクロールが正常に動作
    • スクロールバーが表示され操作可能
  3. 全体的な改善:
    • すべてのルール(100以上)で正しくz-indexが適用される
    • ハイドレーションエラーが発生しない
    • パフォーマンスが向上する

次のステップ

  1. Phase 1の修正を実装
  2. 動作確認(ローカル環境)
  3. Phase 2のデバッグ実施
  4. 必要に応じてPhase 3の改善を実施
  5. 本ドキュメントを更新(修正結果を記録)

関連ファイル

  • apps/web/app/pages/coding-standards/viewer/index.vue - ビューアページ(全体)
  • apps/web/app/pages/coding-standards/viewer/[ruleId].vue - 個別ルールページ
  • apps/web/app/pages/coding-standards/index.vue - 一覧ページ
  • apps/web/public/data/coding-standards.json - ルールデータ