6
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめに

はじめまして。
株式会社ポーラ・オルビスホールディングスのグループデジタルソリューションセンター(IT プロダクト開発チーム)で 3 ヶ月インターンをさせていただいた 42Tokyo の tozeki です。
インターンではスクラム開発のもと、RAG を用いた社内業務規則を対話形式で検索できるチャットアプリの開発に携わりました。
その中で、ChatGPT のように上に伸びる入力欄を作成するのに少し苦労したので、共有します。

使用技術とバージョンは以下です。

  • Next.js - ^16.0.1
  • React - ^19.2.0
  • Material-UI(MUI)- ^7.3.5

なお、ソースコードは実際に作ったチャットアプリではなく、模倣したサンプルコードを使用します。

上に伸びる入力欄を作成するには

UI 要件によっては上方向に伸ばしたい場合もあるのではないでしょうか。
例えば、チャットアプリではユーザー入力欄を画面の下端に固定する UI が一般的です。
この入力欄の高さが変わるとき(改行や長文入力など)、下方向ではなく上方向に伸びることで、入力内容が画面外に隠れることなく、快適に操作できます。

MUI で入力欄を作成できるコンポーネントとしてはTextFieldがあります。
このコンポーネントは改行を入力すると下方向に伸びますが、改行する際に伸びる方向を指定できるスタイルはありません。

input_before.png

では、上方向に伸ばすにはどうするか。
親コンポーネントのスタイルにposition: "fixed", bottom: 0"を入力するだけです。

export type Message = {
  type: "bot" | "user";
  text: string;
};

export default function App() {
  const [chatLog, setChatLog] = useState<Message[]>([]);
  return (
    <Box sx={{ display: "flex" }}>
      {/* サイドバー */}
      <Sidebar />
      {/* メインコンテンツ */}
      <Box
        sx={{
          display: "flex",
          flexDirection: "column",
          height: "100vh",
        }}
      >
        <Header />
        <ChatList chatLog={chatLog} />
        <Box
          sx={{
            width: "100%",
            position: "fixed", //追加
            bottom: 0, //追加
            margin: "0 0 32px",
            zIndex: 100,
          }}
        >
          <UserInput setChatLog={setChatLog} />
        </Box>
      </Box>
    </Box>
  );
}
export default function UserInput({ setChatLog }: UserInputProps) {
  const [userInput, setUserInput] = useState("");

  const handleSend = () => {
    // promptをサーバーに投げる処理
  };

  return (
    <Box
      sx={{
        position: "relative",
        padding: "8px 16px",
        border: "1px solid #ccc",
        borderRadius: "36px",
        boxShadow: "0px 2px 4px rgba(0, 0, 0, 0.1)",
        gap: 2,
        display: "flex",
      }}
    >
      <TextField
        variant="outlined"
        placeholder="メッセージを入力してください"
        value={userInput}
        onChange={(e) => setUserInput(e.target.value)}
        multiline
        minRows={1}
        maxRows={5}
        sx={{
          width: "90%",
          // 他のスタイル
        }}
      />
      <Button
        variant="contained"
        color="primary"
        onClick={handleSend}
        sx={{
          height: "60px",
          width: "60px",
          position: "absolute",
          bottom: 8,
          right: 24,
          borderRadius: "50%",
        }}
      >
        <Send sx={{ fontSize: "24px" }} />
      </Button>
    </Box>
  );
}

bottom: 0で固定すると下側に伸びるスペースがなくなるため、結果的に上方向へ高さが増える挙動になります。

これで簡単に上に伸びる入力欄が実装できます。

input_after_1.png

input_after_2.png

レスポンシブデザインに対応するには

ただし、この方法では対応できないケースがあります。
それは、サイドバーを設置した場合です。

sidebar_before.png

position: fixed を使うと、その要素は画面(ビューポート)に対して固定され、親や周りの要素とは独立した別の要素として扱われます。

この状態では、親がレスポンシブ対応していても、fixed の要素には親要素由来のスタイル(相対的な幅や高さ調節)が適用されません

なんで入力欄が右に押し出されてるの?
position: fixed を使うと、その要素は画面(ビューポート)に対して固定されると言いましたが、
画像を見ると、サイドバーが展開されたときに入力欄の位置が右に押し出されています。
これは、親コンポーネントのスタイルにbottom: 0しか設定しておらず、左右の値は設定していないため、左右位置のみレスポンシブ対応しています。
left: 0も設定すれば、サイドバーが展開されたときの左右位置も固定されます。

その結果、サイドバーの表示/非表示や画面幅の変化に合わせて位置が調整されず、
レスポンシブデザインに対応できなくなります。

これを解決するためにも、fixed で固定しない方法でページの一番下に配置するしかありません。

解決策としては以下のとおりです。

  • 表示領域全体の高さを設定する箇所でheight: "100vh"スタイルを設定
    (コード内でのメインコンテンツ部分)
  • 会話一覧コンポーネントの高さを設定する箇所でflexGrow: 1スタイルを追加
export default function App() {
  const [chatLog, setChatLog] = useState<Message[]>([]);
  return (
    <Box sx={{ display: "flex" }}>
      {/* サイドバー */}
      <Sidebar />
      {/* メインコンテンツ */}
      <Box
        sx={{
          display: "flex",
          flexDirection: "column",
          height: "100vh", // 表示領域全体の高さを指定
        }}
      >
        <Header />
        <ChatList chatLog={chatLog} />
        <Box
          sx={{
            // width: { xs: "100%", lg: "100%" }, //不要
            // position: "fixed", //削除
            // bottom: 0, //削除
            margin: "0 0 32px",
            zIndex: 100,
          }}
        >
          <UserInput setChatLog={setChatLog} />
        </Box>
      </Box>
    </Box>
  );
}
export default function ChatList({ chatLog, flexgrow = 0 }: ChatListProps) {
  return (
    <Box
      sx={{
        padding: { xs: "0px 8px", lg: "0px 16px" },
        fontFamily: "typography.fontFamily",
        flexGrow: 1, //追加
      }}
    >
      {chatLog.map((message, index) => (
        <Typography
          key={index}
          variant="body1"
          gutterBottom
          sx={{
            backgroundColor: message.type === "user" ? "#dededeff" : "none",
            borderRadius: message.type === "user" ? "16px" : 0,
            width: message.type === "user" ? "fit-content" : "auto",
            marginLeft: message.type === "user" ? "auto" : 0,
            padding: "4px 8px",
          }}
        >
          {message.text}
        </Typography>
      ))}
    </Box>
  );
}

これらを設定すれば、メインコンテンツ内で余った余白を会話一覧コンポーネントが自動で埋めてくれます。
position: fixedで固定もしてないため、レスポンシブデザインにも対応できます。

sidebar_after.png

終わりに

実際のアプリではサイドバーがなかったため、position: fixedのままで実装を進めてしまいました。
ですが、インターン後に調べている際に「レスポンシブデザインに対応できない」という問題が見つかり、CSS レイアウトの難しさを実感しました。
同じような課題で悩んでいる方の参考になれば幸いです。

参考にした記事

インターンの感想

このインターンを通して、実際に運用されるサービスを開発することは大きな経験になりました。
これまでの学校課題は、使用技術が事前に決められていて、クリアすれば保守の必要がありませんでしたが、
インターンでは、求められた要求を解決するためにどんな技術を採用するか、運用するならどれほどのコストがかかるかを計算する必要がありました。
こうした判断や計算は、このインターンでしか学べない貴重な経験だったと思います。

また、チームで開発する上で、ブランチ戦略とタスク管理の重要性も強く感じました。
大きな機能を追加するときに、明確な決めごとがないまま各自が作業を進めた結果、コンフリクトが頻繁に発生し、解消に多くの時間を費やしてしまいました。
ブランチ戦略という概念を知らなかったこともあり、ここを意識していれば、より効率的にチームが作業できたと後悔しています。
さらに、タスク管理においても、事前に他部署の承認が必要なことを把握できておらず、作業が完全にストップしてしまったことがありました。
タスクの完了が外部に依存する場合は、事前に働きかけることの重要性を実感しました。
これらの反省点を踏まえて、今後のチーム開発に活かしていきたいです。

振り返ってみると、とても楽しいインターンでした。
早く出勤して開発したい、もう少し残って続きをやりたいと思うくらい、毎週のインターンが楽しみでした。
これも、話しやすい・質問しやすい雰囲気を作ってくださった社員のみなさんや、困ったときにいつでも助けてくれたメンバーのおかげだと思っています。
3 ヶ月間、本当にありがとうございました。

6
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?