🎉

React/Next.jsでメール管理システムのUIを改善した話 〜ダイアログからスライドパネルへの移行〜

に公開

概要

メール管理システムの検索UIにおいて、ユーザビリティの課題を解決するため、ダイアログ形式からスライドパネル形式への移行を実施しました。この記事では、なぜこの変更が必要だったのか、どのように実装したのか、そして実装過程で学んだことを共有します。

解決したかった課題

私たちのメール管理システムでは、以下のような UX 上の課題がありました:

Before: 従来のUI構成

画面構成:

  • メール一覧テーブル(全画面表示)
  • 詳細表示はモーダルダイアログ(背景が暗くなり操作不可)
  • 各行の右端に小さな虫眼鏡アイコンボタン

テーブル列構成:

アイコン 件名 送信者 顧客 サイズ 日時 操作
📎 案件の相談 田中 A社 2KB 09/04 👁️
面談依頼 佐藤 B社 1KB 09/03 👁️

課題点

  1. コンテキストの喪失: モーダルダイアログを開くと、背後のメール一覧が操作できなくなる
  2. 比較の困難さ: 複数のメールを素早く比較したいのに、いちいちダイアログを閉じる必要がある
  3. 情報の冗長性: テーブルに不要な列(顧客・サイズ)が多く、スペースが有効活用されていない
  4. 操作の煩雑さ: 小さな虫眼鏡アイコンをクリックする必要があり、ターゲットが小さく操作しづらい

技術スタック

  • フレームワーク: Next.js 14 (App Router)
  • UI ライブラリ: React 18
  • スタイリング: Tailwind CSS
  • コンポーネント: shadcn/ui
  • 型安全性: TypeScript
  • 状態管理: React Hooks (useState, useCallback, useMemo)

実装内容

After: 改善後のUI構成

画面構成:

  • 画面を 6:4 の比率で分割
  • 左側: メール一覧(常に操作可能)
  • 右側: 詳細パネル(スライドイン表示)
  • オーバーレイなし(背景は操作可能のまま)

改善されたテーブル列構成:

アイコン 件名 送信者 日時
📎 案件の相談 田中 09/04
面談依頼 佐藤 09/03

改善ポイント

  • 行全体がクリック可能 - 小さなアイコンではなく行全体がクリックターゲット
  • リストとパネルの同時表示 - 6:4の比率で画面分割し、両方を同時に操作可能
  • 選択状態の可視化 - 選択中のメールは左側に青いボーダーと背景色で強調
  • 不要な列を削除 - 顧客・サイズ列を削除してスペース効率化
  • シンプルなフィルター - 案件・人材の2つのチェックボックスのみ

1. ダイアログからスライドパネルへの変更

Before: モーダルダイアログ

// 従来のダイアログ実装
export function EmailPreviewDialog({
  email,
  open,
  onOpenChange,
}: EmailPreviewDialogProps) {
  return (
    <Dialog open={open} onOpenChange={onOpenChange}>
      <DialogContent className="max-w-3xl max-h-[80vh]">
        {/* メール詳細内容 */}
      </DialogContent>
    </Dialog>
  )
}

After: スライドパネル

// 新しいスライドパネル実装
export function EmailPreviewPanel({
  email,
  emailDetail,
  open,
  onOpenChange,
}: EmailPreviewPanelProps) {
  return (
    <div className="h-full">
      <div className="bg-background border rounded-md shadow-sm h-[calc(100vh-120px)]">
        <div className="flex flex-col h-full">
          {/* ヘッダー */}
          <div className="flex items-center justify-between p-4 border-b">
            <h2 className="text-lg font-semibold">メール詳細</h2>
            <Button variant="ghost" size="icon" onClick={() => onOpenChange(false)}>
              <X className="h-4 w-4" />
            </Button>
          </div>

          {/* スクロール可能なコンテンツ */}
          <ScrollArea className="flex-1 p-4">
            {/* メール詳細内容 */}
          </ScrollArea>
        </div>
      </div>
    </div>
  )
}

2. レスポンシブなグリッドレイアウトの実装

パネルの開閉に応じて、動的にレイアウトを調整する仕組みを実装しました:

// 動的なグリッドレイアウト
<div className="grid grid-cols-10 gap-4">
  {/* メール一覧 */}
  <div className={`transition-all duration-300 ${
    previewOpen ? 'col-span-6' : 'col-span-10'
  }`}>
    <EmailList />
  </div>

  {/* プレビューパネル */}
  {previewOpen && (
    <div className="col-span-4">
      <EmailPreviewPanel />
    </div>
  )}
</div>

3. テーブル行全体をクリック可能に

ユーザビリティ向上のため、小さなアイコンボタンではなく、行全体をクリック可能にしました:

// 行全体をクリック可能に
<TableRow
  key={email.id}
  className={`h-12 cursor-pointer transition-all duration-200 ${
    selectedEmail?.id === email.id
      ? 'bg-primary/15 border-l-4 border-l-primary'
      : 'hover:bg-muted/60'
  }`}
  onClick={() => handlePreviewOpen(email)}
>
  {/* テーブルセルの内容 */}
</TableRow>

4. Server Actions を使った効率的なデータフェッチ

完全なメール内容を必要な時だけ取得する仕組みを実装:

// Server Action
export async function fetchEmailDetail(
  emailId: string,
  organizationId?: string
): Promise<EmailDetail | null> {
  const emailRepository = createEmailRepository()
  const emailDetail = await emailRepository.findById(emailId)

  // セキュリティチェック
  if (emailDetail?.organizationId !== organizationId) {
    return null
  }

  return emailDetail
}

// クライアント側での使用
const handlePreviewOpen = async (email: Email) => {
  setPreviewEmail(email)
  setPreviewOpen(true)
  setIsLoadingDetail(true)

  try {
    const detail = await fetchEmailDetail(email.id, organizationId)
    setPreviewEmailDetail(detail)
  } catch (error) {
    console.error('Failed to fetch email detail:', error)
  } finally {
    setIsLoadingDetail(false)
  }
}

5. 無限スクロール時の重複防止

// 重複を防ぐロジック
setEmails((prev) => {
  const existingIds = new Set(prev.map(email => email.id))
  const newEmails = result.emails.filter(
    email => !existingIds.has(email.id)
  )
  return [...prev, ...newEmails]
})

操作フローの比較

Before: 従来の操作フロー

  1. 小さな虫眼鏡アイコンをクリック(ターゲットが小さい)
  2. ダイアログが表示され、背景が暗くなる
  3. この時点で一覧は操作不可
  4. 次のメールを見るには、一度ダイアログを閉じる必要がある
  5. 手順1に戻る

After: 改善後の操作フロー

  1. メールの行全体をクリック(ターゲットが大きい)
  2. 右側にパネルがスライド表示
  3. 一覧は引き続き操作可能
  4. 別のメールをクリックすると、パネル内容が即座に切り替わる
  5. 比較しながら素早くメールを確認可能

学んだこと

1. オーバーレイの功罪

最初はスライドパネルにもオーバーレイを付けていましたが、これがユーザーの「複数のメールを比較したい」というニーズを阻害していることに気づきました。オーバーレイを削除することで、リストとパネルの同時操作が可能になり、UXが大幅に向上しました。

2. TypeScript の型定義の重要性

NextAuth のセッション型を拡張する際、型定義ファイルが .gitignore に含まれていて苦労しました:

// types/next-auth.d.ts
declare module 'next-auth' {
  interface Session {
    user: {
      organizationId?: string
    } & DefaultSession['user']
  }
}

3. CSS クラスの特異性

選択中のメールに左側のボーダーを表示する際、border-primary だけだと下部にもボーダーが表示されてしまいました。border-l-primary と明示的に指定することで解決:

/* 誤った実装 */
.selected {
  @apply border-l-4 border-primary; /* 全方向にボーダーが適用される可能性 */
}

/* 正しい実装 */
.selected {
  @apply border-l-4 border-l-primary; /* 左側のみにボーダー */
}

4. Server Component と Client Component の境界

Server Actions を使うことで、必要な時だけデータをフェッチし、初期表示を高速化できました。ただし、組織IDなどのセキュリティ情報の扱いには注意が必要です。

他の開発者への応用可能性

この実装パターンは、以下のようなケースで応用できます:

  1. マスター・ディテール UI: 一覧と詳細を同時に表示したい場合
  2. 比較検討が必要な UI: 複数のアイテムを素早く切り替えて確認したい場合
  3. コンテキスト維持が重要な UI: 背景の情報を見ながら操作したい場合

実装のポイント

  • レスポンシブなグリッドシステムを使う
  • アニメーション(transition-all)で変化を滑らかに
  • 選択状態を視覚的に明確にする
  • データフェッチは必要最小限に

まとめ

ダイアログからスライドパネルへの移行は、単なるUI の変更以上の価値がありました。ユーザーの実際の使い方を理解し、それに合わせてUIを最適化することの重要性を改めて認識しました。

技術的には複雑な実装ではありませんが、ユーザビリティの観点から大きな改善となりました。特に、「複数のメールを素早く確認したい」というユーザーのニーズに応えることができ、作業効率が大幅に向上したという feedback を得ています。

今後の展望

  • キーボードショートカットの実装(矢印キーでメール選択)
  • パネルのリサイズ機能
  • メール本文の検索ハイライト強化
  • モバイル対応の最適化

この記事で紹介したコードは、実際のプロダクションコードを簡略化したものです。エラーハンドリングやセキュリティチェックなど、実際の実装では追加の考慮事項があります。

関連技術: React, Next.js, TypeScript, Tailwind CSS, Server Actions, shadcn/ui

筆者: 91works開発チーム

91works Tech Blog

Discussion