この記事を読むと
透明モーダルを活用してコンポーネント外のタップを検知してコンポーネント内の要素を非表示にさせる方法がわかります。
実現したかったことの詳細
カレンダーアプリの実装をしていて予定を登録する画面を作っていました。UIはIOSにデフォルトで入っている画面を参考に作っていました。
このように開始時間を選ぶときにすぐ下にカレンダーを表示させて選択できるようにしようとしました。このときに常時カレンダーを表示させていると画面を占めるカレンダーの割合が多くなり、ユーザー視点にたつと非常に見にくいものになってしまいます。なので常時表示はせず、選択されたときのみカレンダーを表示するようにしたかったです。
## 問題のある実装
// ❌ コンポーネント内部のタップにしか反応できない
<TouchableWithoutFeedback onPress={handleOutsidePress}>
<View>
<FormControl>
{/* 入力フィールド */}
<Pressable onPress={handleTogglePicker}>
<Input />
</Pressable>
{/* ピッカー */}
{showPicker && (
<View>
<SomePicker />
</View>
)}
</FormControl>
</View>
</TouchableWithoutFeedback>
まず最初にTouchableWithoutFeedbackを使用して他のところを触ったときに閉じるようにしようとしました。しかしこれには以下の問題があります。
1.TouchableWithoutFeedbackはラップしているView内でのタップしか検知できない
2.コンポーネント外(例:画面の他の部分)をタップしてもピッカーが閉じない
コンポーネント内の他の場所をタップしたときは意図したとおりに動くのですが、ほかのコンポーネントを触ってもカレンダーが閉じません。これではカレンダーが不必要なときにも表示されてしまいます。
## 解決策:透明Modalによる外部タップ検知
この問題を解決するために、透明なModalを活用します:
export const DateTimeInputField = ({ ... }) => {
const [showDatePicker, setShowDatePicker] = useState(false);
const [showTimePicker, setShowTimePicker] = useState(false);
const closePickers = () => {
setShowDatePicker(false);
setShowTimePicker(false);
};
return (
<View>
<FormControl>
{/* 通常の入力フィールドとピッカー */}
<VStack space="md">
<HStack space="md">
<Pressable onPress={() => setShowDatePicker(!showDatePicker)}>
<Input />
</Pressable>
<Pressable onPress={() => setShowTimePicker(!showTimePicker)}>
<Input />
</Pressable>
</HStack>
{/* ピッカーはインライン表示(デザイン変更なし) */}
{showDatePicker && (
<View className="bg-background-0 p-4">
<Calendar />
</View>
)}
{showTimePicker && (
<View className="bg-background-0 p-4">
<TimePicker />
</View>
)}
</VStack>
</FormControl>
{/* ✅ 外部タップ検知用の透明Modal */}
{(showDatePicker || showTimePicker) && (
<Modal
visible={true}
transparent={true}
animationType="none"
onRequestClose={closePickers}
>
<Pressable
style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0
}}
onPress={closePickers}
/>
</Modal>
)}
</View>
);
};
実装のポイント
1.透明なModalの設定
<Modal
visible={true}
transparent={true} // 背景を透明に
animationType="none" // アニメーションなしで即座に表示
onRequestClose={closePickers} // Androidの戻るボタン対応
>
背景を透明にすることによりモーダルは存在するのですが表示はされないようにします。
- 画面全体をカバーするPressable
<Pressable
style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0
}}
onPress={closePickers}
/>
上のモーダル内にPressableをおき押されたときにclosePicker関数が呼ばれるようにします。absolute,0,0,0,0にすることによって画面全体を覆うことができてどこを触っても反応するようになります。
- 条件付き表示
{(showDatePicker || showTimePicker) && (
<Modal>
{/* ... */}
</Modal>
)}
カレンダーが表示されているときのみにmodalが表示(存在)するようにします。これにより無駄な表示や関数の呼び出しを防ぐことができてパフォーマンスの向上につながります。
## まとめ
TouchableWithoutFeedbackの制限を克服するために透明なModalを活用することで、デザインを変更せずにコンポーネント外部のタップを検知できるようになります。この手法により、より直感的なユーザーエクスペリエンスを提供できます。