diff --git a/example/app/(tabs)/index.tsx b/example/app/(tabs)/index.tsx
index 4afd5fe3..08f8c1bf 100644
--- a/example/app/(tabs)/index.tsx
+++ b/example/app/(tabs)/index.tsx
@@ -1,164 +1,168 @@
-import { Link, type LinkProps } from "expo-router";
-import { useCallback } from "react";
-import { type LayoutChangeEvent, Platform, Pressable, StyleSheet, useColorScheme, View } from "react-native";
-import { SafeAreaView } from "react-native-safe-area-context";
+import { Link, type LinkProps } from 'expo-router';
+import { useCallback } from 'react';
+import { type LayoutChangeEvent, Platform, Pressable, StyleSheet, useColorScheme, View } from 'react-native';
+import { SafeAreaView } from 'react-native-safe-area-context';
-import { LegendList } from "@legendapp/list";
-import { useBottomTabBarHeight } from "@react-navigation/bottom-tabs";
-import { ThemedText } from "~/components/ThemedText";
-import { ThemedView } from "~/components/ThemedView";
+import { LegendList } from '@legendapp/list';
+import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs';
+import { ThemedText } from '~/components/ThemedText';
+import { ThemedView } from '~/components/ThemedView';
// @ts-expect-error nativeFabricUIManager is not defined in the global object types
-export const IsNewArchitecture = Platform.OS === "web" || global.nativeFabricUIManager != null;
+export const IsNewArchitecture = Platform.OS === 'web' || global.nativeFabricUIManager != null;
type ListElement = {
id: number;
title: string;
- url: LinkProps["href"];
+ url: LinkProps['href'];
index: number;
};
const data: ListElement[] = [
{
- title: "Bidirectional Infinite List",
- url: "/bidirectional-infinite-list",
+ title: 'Bidirectional Infinite List',
+ url: '/bidirectional-infinite-list',
},
{
- title: "Chat example",
- url: "/chat-example",
+ title: 'Chat example',
+ url: '/chat-example',
},
{
- title: "AI Chat",
- url: "/ai-chat",
+ title: 'AI Chat',
+ url: '/ai-chat',
},
{
- title: "Infinite chat",
- url: "/chat-infinite",
+ title: 'Infinite chat',
+ url: '/chat-infinite',
},
{
- title: "Countries List",
- url: "/countries",
+ title: 'Countries List',
+ url: '/countries',
},
{
- title: "Countries with headers",
- url: "/countries-with-headers",
+ title: 'Countries with headers',
+ url: '/countries-with-headers',
},
{
- title: "Countries with headers fixed",
- url: "/countries-with-headers-fixed",
+ title: 'Countries with headers fixed',
+ url: '/countries-with-headers-fixed',
},
{
- title: "Countries with headers sticky",
- url: "/countries-with-headers-sticky",
+ title: 'Countries with headers sticky',
+ url: '/countries-with-headers-sticky',
},
{
- title: "Lazy List",
- url: "/lazy-list",
+ title: 'Lazy List',
+ url: '/lazy-list',
},
{
- title: "Always render",
- url: "/always-render",
+ title: 'Always render',
+ url: '/always-render',
},
{
- title: "MVCP test",
- url: "/mvcp-test",
+ title: 'MVCP test',
+ url: '/mvcp-test',
},
{
- title: "Accurate scrollToIndex",
- url: "/accurate-scrollto",
+ title: 'Accurate scrollToIndex',
+ url: '/accurate-scrollto',
},
{
- title: "Accurate scrollToIndex 2",
- url: "/accurate-scrollto-2",
+ title: 'Accurate scrollToIndex 2',
+ url: '/accurate-scrollto-2',
},
{
- title: "Columns",
- url: "/columns",
+ title: 'Columns',
+ url: '/columns',
},
{
- title: "Product shelf",
- url: "/product-shelf",
+ title: 'Product shelf',
+ url: '/product-shelf',
},
{
- title: "Cards Columns",
- url: "/cards-columns",
+ title: 'Cards Columns',
+ url: '/cards-columns',
},
{
- title: "Chat keyboard",
- url: "/chat-keyboard",
+ title: 'Chat keyboard',
+ url: '/chat-keyboard',
},
{
- title: "Movies FlashList",
- url: "/movies-flashlist",
+ title: 'Chat keyboard overlap',
+ url: '/chat-keyboard-overlap',
},
{
- title: "Initial scroll index precise navigation",
- url: "/initial-scroll-index",
+ title: 'Movies FlashList',
+ url: '/movies-flashlist',
},
{
- title: "Initial scroll index(free element height)",
- url: "/initial-scroll-index-free-height",
+ title: 'Initial scroll index precise navigation',
+ url: '/initial-scroll-index',
},
{
- title: "Initial scroll index(start at the end)",
- url: "/initial-scroll-start-at-the-end",
+ title: 'Initial scroll index(free element height)',
+ url: '/initial-scroll-index-free-height',
},
{
- title: "Initial Scroll Index keyed",
- url: "/initial-scroll-index-keyed",
+ title: 'Initial scroll index(start at the end)',
+ url: '/initial-scroll-start-at-the-end',
},
{
- title: "Mutable elements",
- url: "/mutable-cells",
+ title: 'Initial Scroll Index keyed',
+ url: '/initial-scroll-index-keyed',
},
{
- title: "Extra data",
- url: "/extra-data",
+ title: 'Mutable elements',
+ url: '/mutable-cells',
},
{
- title: "Countries List(FlashList)",
- url: "/countries-flashlist",
+ title: 'Extra data',
+ url: '/extra-data',
},
{
- title: "Filter elements",
- url: "/filter-elements",
+ title: 'Countries List(FlashList)',
+ url: '/countries-flashlist',
},
{
- title: "Video feed",
- url: "/video-feed",
+ title: 'Filter elements',
+ url: '/filter-elements',
},
{
- title: "Countries Reorder",
- url: "/countries-reorder",
+ title: 'Video feed',
+ url: '/video-feed',
},
{
- title: "Cards FlashList",
- url: "/cards-flashlist",
+ title: 'Countries Reorder',
+ url: '/countries-reorder',
},
{
- title: "Cards no recycle",
- url: "/cards-no-recycle",
+ title: 'Cards FlashList',
+ url: '/cards-flashlist',
},
{
- title: "Cards FlatList",
- url: "/cards-flatlist",
+ title: 'Cards no recycle',
+ url: '/cards-no-recycle',
},
{
- title: "Add to the end",
- url: "/add-to-end",
+ title: 'Cards FlatList',
+ url: '/cards-flatlist',
},
{
- title: "Chat resize outer",
- url: "/chat-resize-outer",
+ title: 'Add to the end',
+ url: '/add-to-end',
},
{
- title: "Accurate scrollToHuge",
- url: "/accurate-scrollto-huge",
+ title: 'Chat resize outer',
+ url: '/chat-resize-outer',
},
{
- title: "Chat keyboard big",
- url: "/chat-keyboard-big",
+ title: 'Accurate scrollToHuge',
+ url: '/accurate-scrollto-huge',
+ },
+ {
+ title: 'Chat keyboard big',
+ url: '/chat-keyboard-big',
},
].map(
(v, i) =>
@@ -171,7 +175,7 @@ const data: ListElement[] = [
const RightIcon = () => ›;
const ListItem = ({ title, url, index }: ListElement) => {
- const theme = useColorScheme() ?? "light";
+ const theme = useColorScheme() ?? 'light';
return (
@@ -179,7 +183,7 @@ const ListItem = ({ title, url, index }: ListElement) => {
@@ -194,7 +198,7 @@ const ListItem = ({ title, url, index }: ListElement) => {
const ListElements = () => {
const height = useBottomTabBarHeight();
const onLayout = useCallback((event: LayoutChangeEvent) => {
- console.log("onlayout", event.nativeEvent.layout);
+ console.log('onlayout', event.nativeEvent.layout);
}, []);
return (
@@ -203,16 +207,16 @@ const ListElements = () => {
estimatedItemSize={60}
keyExtractor={(item) => item.id.toString()}
ListFooterComponent={}
- ListFooterComponentStyle={{ height: Platform.OS === "ios" ? height : 0 }}
+ ListFooterComponentStyle={{ height: Platform.OS === 'ios' ? height : 0 }}
ListHeaderComponent={
-
- {IsNewArchitecture ? "New" : "Old"} Architecture, {__DEV__ ? "DEV" : "PROD"}, 2.0
+
+ {IsNewArchitecture ? 'New' : 'Old'} Architecture, {__DEV__ ? 'DEV' : 'PROD'}, 2.0
}
onItemSizeChanged={(info) => {
- console.log("item size changed", info);
+ console.log('item size changed', info);
}}
onLayout={onLayout}
renderItem={({ item, index }) => }
@@ -227,11 +231,11 @@ const styles = StyleSheet.create({
},
item: {
borderBottomWidth: 1,
- flexDirection: "row",
+ flexDirection: 'row',
height: 60,
- justifyContent: "space-between",
+ justifyContent: 'space-between',
padding: 16,
- width: "100%",
+ width: '100%',
},
});
diff --git a/example/app/chat-keyboard-overlap/index.tsx b/example/app/chat-keyboard-overlap/index.tsx
new file mode 100644
index 00000000..db19b142
--- /dev/null
+++ b/example/app/chat-keyboard-overlap/index.tsx
@@ -0,0 +1,275 @@
+import { type PropsWithChildren, useMemo, useState } from 'react';
+import { BlurView } from 'expo-blur';
+import { Button, type LayoutChangeEvent, Platform, StyleSheet, Text, TextInput, View } from 'react-native';
+import { KeyboardGestureArea, KeyboardProvider, KeyboardStickyView } from 'react-native-keyboard-controller';
+import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
+
+import { KeyboardAvoidingLegendList } from '@legendapp/list/keyboard';
+
+type Message = {
+ id: string;
+ text: string;
+ sender: 'user' | 'bot';
+ timeStamp: number;
+};
+
+let idCounter = 0;
+const MS_PER_SECOND = 1000;
+
+const defaultChatMessages: Message[] = [
+ {
+ id: String(idCounter++),
+ sender: 'user',
+ text: 'Hi, I have a question about your product',
+ timeStamp: Date.now() - MS_PER_SECOND * 5,
+ },
+ {
+ id: String(idCounter++),
+ sender: 'bot',
+ text: 'Hello there! How can I assist you today?',
+ timeStamp: Date.now() - MS_PER_SECOND * 4,
+ },
+ {
+ id: String(idCounter++),
+ sender: 'user',
+ text: "I'm looking for information about pricing plans",
+ timeStamp: Date.now() - MS_PER_SECOND * 4,
+ },
+ {
+ id: String(idCounter++),
+ sender: 'bot',
+ text: 'We offer several pricing tiers based on your needs',
+ timeStamp: Date.now() - MS_PER_SECOND * 4,
+ },
+ {
+ id: String(idCounter++),
+ sender: 'bot',
+ text: 'Our basic plan starts at $9.99 per month',
+ timeStamp: Date.now() - MS_PER_SECOND * 4,
+ },
+ {
+ id: String(idCounter++),
+ sender: 'user',
+ text: 'Do you offer any discounts for annual billing?',
+ timeStamp: Date.now() - MS_PER_SECOND * 4,
+ },
+ {
+ id: String(idCounter++),
+ sender: 'bot',
+ text: 'Yes! You can save 20% with our annual billing option',
+ timeStamp: Date.now() - MS_PER_SECOND * 4,
+ },
+ {
+ id: String(idCounter++),
+ sender: 'user',
+ text: 'That sounds great. What features are included?',
+ timeStamp: Date.now() - MS_PER_SECOND * 4,
+ },
+ {
+ id: String(idCounter++),
+ sender: 'bot',
+ text: 'The basic plan includes all core features plus 10GB storage',
+ timeStamp: Date.now() - MS_PER_SECOND * 4,
+ },
+ {
+ id: String(idCounter++),
+ sender: 'bot',
+ text: 'Premium plans include priority support and additional tools',
+ timeStamp: Date.now() - MS_PER_SECOND * 4,
+ },
+ {
+ id: String(idCounter++),
+ sender: 'user',
+ text: 'I think the basic plan would work for my needs',
+ timeStamp: Date.now() - MS_PER_SECOND * 4,
+ },
+ {
+ id: String(idCounter++),
+ sender: 'bot',
+ text: 'Perfect! I can help you get set up with that',
+ timeStamp: Date.now() - MS_PER_SECOND * 4,
+ },
+ {
+ id: String(idCounter++),
+ sender: 'user',
+ text: 'Thanks for your help so far',
+ timeStamp: Date.now() - MS_PER_SECOND * 4,
+ },
+ {
+ id: String(idCounter++),
+ sender: 'bot',
+ text: "You're welcome! Is there anything else I can assist with today?",
+ timeStamp: Date.now() - MS_PER_SECOND * 3,
+ },
+];
+
+function ChatMessage({ item }: { item: Message }) {
+ return (
+ <>
+
+ {item.text}
+
+
+ {new Date(item.timeStamp).toLocaleTimeString()}
+
+ >
+ );
+}
+
+const ChatKeyboard = () => {
+ const [messages, setMessages] = useState(defaultChatMessages);
+ const [inputText, setInputText] = useState('');
+ const [inputHeight, setInputHeight] = useState(0); // Default estimate
+ const insets = useSafeAreaInsets();
+
+ const handleInputLayout = (event: LayoutChangeEvent) => {
+ const { height } = event.nativeEvent.layout;
+ setInputHeight(height);
+ };
+
+ const contentContainerStyle = useMemo(
+ () => [styles.contentContainer, { paddingBottom: inputHeight }],
+ [inputHeight],
+ );
+
+ const inputContainerStyle = useMemo(
+ () => [styles.inputContainer, { paddingBottom: insets.bottom + 10, marginTop: -inputHeight }],
+ [inputHeight],
+ );
+
+ const sendMessage = () => {
+ const text = inputText || 'Empty message';
+ if (text.trim()) {
+ setMessages((messagesNew) => [
+ ...messagesNew,
+ { id: String(idCounter++), sender: 'user', text: text, timeStamp: Date.now() },
+ ]);
+ setInputText('');
+ setTimeout(() => {
+ setMessages((messagesNew) => [
+ ...messagesNew,
+ {
+ id: String(idCounter++),
+ sender: 'bot',
+ text: `Answer: ${text.toUpperCase()}`,
+ timeStamp: Date.now(),
+ },
+ ]);
+ }, 300);
+ }
+ };
+
+ return (
+
+
+
+ {inputHeight !== 0 && (
+ item.id}
+ maintainScrollAtEnd
+ maintainVisibleContentPosition
+ renderItem={ChatMessage}
+ safeAreaInsetBottom={insets.bottom}
+ style={styles.list}
+ />
+ )}
+
+
+
+
+
+
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ botMessageContainer: {
+ backgroundColor: '#f1f1f1',
+ },
+ botStyle: {
+ alignSelf: 'flex-start',
+ maxWidth: '75%',
+ },
+ container: {
+ backgroundColor: '#fff',
+ flex: 1,
+ },
+ contentContainer: {
+ paddingHorizontal: 16,
+ // paddingTop: 96,
+ // paddingBottom is set dynamically based on input height
+ },
+ input: {
+ backgroundColor: 'white',
+ borderColor: '#ccc',
+ borderRadius: 5,
+ borderWidth: 1,
+ flex: 1,
+ marginRight: 10,
+ padding: 10,
+ },
+ inputContainer: {
+ alignItems: 'center',
+ backgroundColor: 'transparent',
+ borderColor: '#ccc',
+ borderTopWidth: 1,
+ flexDirection: 'row',
+ padding: 10,
+ // marginTop is set dynamically based on input height
+ },
+ list: {
+ flex: 1,
+ },
+ messageContainer: {
+ borderRadius: 16,
+ marginVertical: 4,
+ padding: 16,
+ },
+ messageText: {
+ fontSize: 16,
+ },
+ timeStamp: {
+ marginVertical: 5,
+ },
+ timeStampText: {
+ color: '#888',
+ fontSize: 12,
+ },
+ userMessageContainer: {
+ backgroundColor: '#007AFF',
+ },
+ userMessageText: {
+ color: 'white',
+ },
+ userStyle: {
+ alignItems: 'flex-end',
+ alignSelf: 'flex-end',
+ maxWidth: '75%',
+ },
+});
+
+export default ChatKeyboard;