Skip to content

feat(image-viewer): 同メッセージ内の複数画像を連続して閲覧できるようにする#5093

Open
uni-kakurenbo wants to merge 2 commits intomasterfrom
feat/continuous-image-viewer
Open

feat(image-viewer): 同メッセージ内の複数画像を連続して閲覧できるようにする#5093
uni-kakurenbo wants to merge 2 commits intomasterfrom
feat/continuous-image-viewer

Conversation

@uni-kakurenbo
Copy link
Copy Markdown
Contributor

@uni-kakurenbo uni-kakurenbo commented Mar 24, 2026

概要

画像を動かせる機能と競合するのでスワップは実装してないです

なぜこの PR を入れたいのか

UI 変更部分のスクリーンショット

Before After
image image

PR を出す前の確認事項

  • (機能の追加なら)追加することの合意がチームで取れている
    • 取れていない場合はチェックを外して PR にすれば OK
  • 動作確認ができている
  • 自分で一度コードを眺めて自分的に問題はなさそう

Summary by CodeRabbit

リリース ノート

  • New Features
    • 画像ビューアに前後の画像へ移動できるナビゲーション機能を追加しました。矢印キーとボタンで操作可能です。
    • 関連画像を自動的にプリロード表示するようになりました。
    • スムーズなスライド効果を含むトランジション動作を実装しました。

@github-actions
Copy link
Copy Markdown

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 24, 2026

📝 Walkthrough

Walkthrough

FileModalImage.vueコンポーネントに複数画像ナビゲーション機能を追加。兄弟画像リストを計算して左右ボタンを表示し、矢印キー入力に対応。トランジション効果とプリロード機能を実装。

Changes

Cohort / File(s) Summary
画像ナビゲーション機能
src/components/Modal/FileModal/FileModalImage.vue
複数画像の左右移動機能を実装。兄弟画像IDリストの計算、ナビゲーションボタン(条件付き表示)、矢印キーハンドリング、画像プリロード、方向別トランジション効果(スライド/フェード)を追加。

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed タイトルは「同メッセージ内の複数画像を連続して閲覧できるようにする」という機能の追加を明確に説明しており、実装内容の主要な変更と一致しています。
Linked Issues check ✅ Passed 実装はLinked Issue #5041で要求された「左右のボタンまたはキーボード操作による次/前の画像への移動」をサポートしており、主要な要件に対応しています。
Out of Scope Changes check ✅ Passed FileModalImage.vueの変更は、キーボードナビゲーション、画像プリロード、トランジション効果など、すべて同メッセージ内の複数画像の連続閲覧機能に直接関連しており、スコープ外の変更は見られません。
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description check ✅ Passed PR説明が全体的に完全で、テンプレートの主要セクション(概要、理由、スクリーンショット、確認事項)が揃っている。

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/continuous-image-viewer

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov
Copy link
Copy Markdown

codecov Bot commented Mar 24, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 62.60%. Comparing base (e40810e) to head (42692c4).

Additional details and impacted files
@@           Coverage Diff           @@
##           master    #5093   +/-   ##
=======================================
  Coverage   62.60%   62.60%           
=======================================
  Files         108      108           
  Lines        3097     3097           
  Branches      630      630           
=======================================
  Hits         1939     1939           
  Misses       1048     1048           
  Partials      110      110           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
src/components/Modal/FileModal/FileModalImage.vue (2)

122-136: 編集可能要素の除外条件はもう少し広げたいです。

input / textarea だけだと、contenteditableselect 系の要素上でも ArrowLeft / ArrowRight がそのまま画像遷移になります。HTMLElement.isContentEditableHTMLSelectElement も見ておくと事故りにくいです。

🔧 修正案
 const onKeyDown = (e: KeyboardEvent) => {
-  if (
-    e.target instanceof HTMLInputElement ||
-    e.target instanceof HTMLTextAreaElement
-  ) {
+  const target = e.target instanceof HTMLElement ? e.target : null
+  if (
+    target instanceof HTMLInputElement ||
+    target instanceof HTMLTextAreaElement ||
+    target instanceof HTMLSelectElement ||
+    target?.isContentEditable
+  ) {
     return
   }
   if (e.key === 'ArrowRight') {
     nextFile()
   } else if (e.key === 'ArrowLeft') {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/Modal/FileModal/FileModalImage.vue` around lines 122 - 136,
onKeyDown currently only ignores inputs and textareas, causing
ArrowLeft/ArrowRight to trigger image navigation even when focus is on
contenteditable or select elements; update the onKeyDown handler to first assert
e.target is an HTMLElement and skip handling when e.target.isContentEditable is
true or when e.target is an instance of HTMLSelectElement (in addition to
existing checks for HTMLInputElement and HTMLTextAreaElement) so that
nextFile()/prevFile() are not called while editing or selecting; keep
useEventListener('keydown', onKeyDown) as-is.

89-97: プリロード対象は前後の画像だけに絞った方がよさそうです。

ここだと sibling 全件を毎回 new Image() しているので、画像枚数の多い投稿を開いた瞬間に /files/:id へのリクエストが一気に増えます。体感改善に必要なのは隣接画像だけなので、少なくとも前後 1 枚ずつ、または件数上限付きにしたいです。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/Modal/FileModal/FileModalImage.vue` around lines 89 - 97, The
current watchEffect iterates all siblingImageIds and creates an Image for each,
causing a spike of requests; change it to preload only the adjacent images
(previous and next) relative to props.fileId (or at most a small fixed N
neighbors) by finding the index of props.fileId in siblingImageIds and only
calling new Image() with buildFilePath for siblingImageIds[index-1] and
siblingImageIds[index+1] (or a bounded slice like slice(max(0,index-N),
min(len,index+N+1))). Keep the same watchEffect but replace the full forEach
with logic that computes the neighbor ids and skips undefined or equal ids
before creating Image instances.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/Modal/FileModal/FileModalImage.vue`:
- Around line 7-13: Replace the invisible hover-only navigation divs in
FileModalImage.vue with semantic buttons so touch users can discover and operate
them: change the left/right nav <div> elements that call prevFile and nextFile
into <button type="button"> elements (keep `@click.stop`="prevFile"/"nextFile" and
the class bindings $style.navButton, $style.navLeft, $style.navRight), add
accessible attributes (aria-label like "Previous image"/"Next image" and ensure
focusability), and update CSS so visibility is not solely :hover — make the
controls visible on :focus-visible and for touch devices (e.g. pointer:coarse
media query) or remove opacity gating while keeping hover styling as
enhancement; also ensure hidden states remove pointer-events rather than leaving
invisible hit areas.

---

Nitpick comments:
In `@src/components/Modal/FileModal/FileModalImage.vue`:
- Around line 122-136: onKeyDown currently only ignores inputs and textareas,
causing ArrowLeft/ArrowRight to trigger image navigation even when focus is on
contenteditable or select elements; update the onKeyDown handler to first assert
e.target is an HTMLElement and skip handling when e.target.isContentEditable is
true or when e.target is an instance of HTMLSelectElement (in addition to
existing checks for HTMLInputElement and HTMLTextAreaElement) so that
nextFile()/prevFile() are not called while editing or selecting; keep
useEventListener('keydown', onKeyDown) as-is.
- Around line 89-97: The current watchEffect iterates all siblingImageIds and
creates an Image for each, causing a spike of requests; change it to preload
only the adjacent images (previous and next) relative to props.fileId (or at
most a small fixed N neighbors) by finding the index of props.fileId in
siblingImageIds and only calling new Image() with buildFilePath for
siblingImageIds[index-1] and siblingImageIds[index+1] (or a bounded slice like
slice(max(0,index-N), min(len,index+N+1))). Keep the same watchEffect but
replace the full forEach with logic that computes the neighbor ids and skips
undefined or equal ids before creating Image instances.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d830822b-93a1-466d-b96e-e6ba985b828f

📥 Commits

Reviewing files that changed from the base of the PR and between e40810e and 42692c4.

📒 Files selected for processing (1)
  • src/components/Modal/FileModal/FileModalImage.vue

Comment on lines +7 to +13
<div
v-if="siblingImageIds.length > 1"
:class="[$style.navButton, $style.navLeft]"
@click.stop="prevFile"
>
<AIcon name="chevron-left" mdi :size="32" />
</div>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

タッチ端末では発見可能な遷移 UI がなくなっています。

表示条件が :hover 依存なので、hover のない端末では左右ボタンが見えません。しかも opacity: 0 のままでも hit area は残るので、見えないタップ領域だけが左右端に残ります。この PR では swipe を入れていないので、モバイルだと前後移動の手段が実質失われます。div ではなく <button> にして、非 hover 端末と :focus-visible では常に操作可能にしたいです。

🔧 修正案
-    <div
+    <button
+      type="button"
+      aria-label="前の画像へ"
       v-if="siblingImageIds.length > 1"
       :class="[$style.navButton, $style.navLeft]"
       `@click.stop`="prevFile"
     >
       <AIcon name="chevron-left" mdi :size="32" />
-    </div>
+    </button>
...
-    <div
+    <button
+      type="button"
+      aria-label="次の画像へ"
       v-if="siblingImageIds.length > 1"
       :class="[$style.navButton, $style.navRight]"
       `@click.stop`="nextFile"
     >
       <AIcon name="chevron-right" mdi :size="32" />
-    </div>
+    </button>
 .navButton {
   `@include` color-ui-secondary;

   position: absolute;
   top: 50%;
   transform: translateY(-50%);
   z-index: $z-index-file-modal-header;
   cursor: pointer;
   padding: 4px;
   border-radius: 50%;
+  border: 0;
+  color: inherit;
+  appearance: none;
   background: rgba(0, 0, 0, 0.3);
   display: flex;
   align-items: center;
   justify-content: center;
   transition: all 0.2s ease;
   opacity: 0;
+  pointer-events: none;

   .container:hover & {
     opacity: 1;
+    pointer-events: auto;
   }
+
+  &:focus-visible {
+    opacity: 1;
+    pointer-events: auto;
+  }
+
+  `@media` (hover: none), (pointer: coarse) {
+    opacity: 1;
+    pointer-events: auto;
+  }

   &:hover {
     background: rgba(0, 0, 0, 0.6);
   }
 }

Also applies to: 33-39, 192-211

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/Modal/FileModal/FileModalImage.vue` around lines 7 - 13,
Replace the invisible hover-only navigation divs in FileModalImage.vue with
semantic buttons so touch users can discover and operate them: change the
left/right nav <div> elements that call prevFile and nextFile into <button
type="button"> elements (keep `@click.stop`="prevFile"/"nextFile" and the class
bindings $style.navButton, $style.navLeft, $style.navRight), add accessible
attributes (aria-label like "Previous image"/"Next image" and ensure
focusability), and update CSS so visibility is not solely :hover — make the
controls visible on :focus-visible and for touch devices (e.g. pointer:coarse
media query) or remove opacity gating while keeping hover styling as
enhancement; also ensure hidden states remove pointer-events rather than leaving
invisible hit areas.

@uni-kakurenbo uni-kakurenbo self-assigned this Mar 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

投稿に画像が複数枚含まれるとき、左右にスワイプして隣の画像を見たい

1 participant