概要
今回は、LINE BotでGoogle Calendarと連携した予約UIを作る中で、Flex Messageのボタンを「タップ不可」にするUIを作ってみました!
作ったのは、時間帯選択のFlex Messageです。Google Calendarの空き情報を取得して、空いているスロットは緑、予定が入っているスロットはグレーで表示します。空きスロットをタップすると予約フローに進み、グレーのスロットはタップしても何も起きない、という挙動です。
シンプルなんですが、Flex Messageの仕様上「グレーにしたボタンをタップしても何も起きない」を解決するために埋まりスロットには button ではなく box + text を使うことで解決しています。以下が実際の画像です。
なぜ Flex Message でタップ無効の UI が作れないのか
button の action は必須
原因は、Flex Message の button コンポーネントでは action が必須プロパティ になっていることでした。LINE の公式 OpenAPI スキーマ(line-openapi)を確認すると、FlexButton の定義は以下のようになっています。
FlexButton:
type: object
required:
- action # ← 必須
allOf:
- "$ref": "#/components/schemas/FlexComponent"
- type: object
properties:
action:
"$ref": "#/components/schemas/Action"
color:
type: string
style:
type: string
enum:
- primary
- secondary
- link
required: [action] と明記されています。action を省略するとバリデーションエラーになり、メッセージ自体が送信できません。button を使う限り、どんなに見た目をグレーにしても中身には必ず action が入っていて、タップすれば発火してしまいます。
disabled 属性は存在しない
HTMLなら <button disabled> の一言で終わる話ですが、Flex Messageにはそのような disabled に相当する属性は存在しません。スキーマの FlexButton のプロパティを見ても、action、color、style、height、gravity などがあるだけで、タップを無効化するようなフラグはどこにもないです。
つまり、button コンポーネントを使っている限り、「見た目だけグレーにして押せなくする」は仕様上不可能ということになります。
box なら action は省略できる
一方で、FlexBox のスキーマを見てみます。
FlexBox:
type: object
required:
- layout
- contents # ← action は required に入っていない
allOf:
- properties:
layout:
type: string
contents:
type: array
backgroundColor:
type: string
cornerRadius:
type: string
action: # ← プロパティ自体は存在する
"$ref": "#/components/schemas/Action"
justifyContent:
type: string
box にも action プロパティ自体は存在しますが、required に含まれていません。つまり省略可能で、省略すればタップしても何も起きません。
ここがポイントで、box の中に text を置いてボタンと同じ見た目にスタイリングすれば、見た目はボタン、タップしても無反応なコンポーネントが作れるわけです。
今回実装したこと
box + text でボタンの見た目を再現
空きスロットは従来どおり button + postback、埋まりスロットだけ box + text に差し替える形で実装しました。
if is_busy:
# box + text: タップしても何も起きない
element = {
"type": "box",
"layout": "vertical",
"contents": [{
"type": "text",
"text": f"{start} - {end}",
"align": "center",
"color": "#FFFFFF",
"size": "sm",
}],
"backgroundColor": "#CCCCCC",
"cornerRadius": "md",
"height": "40px",
"justifyContent": "center",
"margin": "sm",
}
else:
# button: タップで postback
element = {
"type": "button",
"action": {
"type": "postback",
"label": f"{start} - {end}",
"data": f"action=select_time&date={date}&start={start}&end={end}",
},
"style": "primary",
"color": "#06C755",
"height": "sm",
"margin": "sm",
}
button と box は同じ body 内に混在できるので、スロットごとに出し分けるだけでOKです。
見た目を揃えるために意識したこと
box をそのまま使うと button と見た目が微妙にずれるので、3つのポイントを意識しました。
背景色の指定方法が違う。 button は color プロパティで背景色を指定しますが、box は backgroundColor です。プロパティ名が違うだけなので、同じ色コードを入れれば見た目は揃います。
角丸はデフォルトでつかない。 button は最初から角丸が適用されていますが、box はデフォルトが直角です。cornerRadius: "md" を指定することで button と同じ丸みが再現できます。
テキストが上に寄る。 button の label は自動で上下中央に配置されますが、box + text だとテキストが上に寄ります。justifyContent: "center" と height: "40px" を明示的に指定して揃えました。
日付選択でも同じパターンを適用
この方法は時間帯選択だけでなく、日付選択カルーセルでも使っています。2週間分の日付を並べて、Google Calendarに予定がある日はグレーの box、空いている日は緑の button で表示しています。コンポーネントの構造は全く同じで、ラベルを日付に変えるだけなので汎用的に使えました。
バックエンド側のガード処理が不要に
副次的なメリットとして、Webhookハンドラ側のコードがシンプルになりました。もし button のまま実装していたら、ハンドラ側で「このスロットはbusyだから処理をスキップする」というガード処理を入れる必要がありました。Flex Message側で確実にブロックできていれば、ハンドラには余計な分岐を持ち込まなくて済みます。
まとめ
button では「押せないボタン」は作れない
Flex Messageの button は action が必須プロパティで、disabled 属性も存在しない。背景色をグレーにしても postback は発火する。
box + text で代替する
box の action はオプショナルなので、省略すればタップしても無反応。backgroundColor / cornerRadius / justifyContent の3つを調整すれば button との見た目の差はほぼなくなる。
box に action を付けないこと
box にも action を付与できるが、付けてしまうと button と同じ問題が再発する。あくまで action を省略することで無反応にする手法。

