Skip to content

Commit ddfc17a

Browse files
fpenamarkrickertfrankcaliselindboe
authored
Improve keyboard avoid behavior for section list and Screen component with "fixed" preset variant (#2722)
* Temporary solution * Lint fix * Add new component for section and aware scroll view * Add documentation * Add new library to doc * Major version instead of major and minor * Fix lint issue * Move SectionListWithKeyboardAwareScrollView outside base components * Move component outside and match snapshot * Update import * fix: Add some necessary boilerplate linter plugins Linter was failing before. Needed these dev deps. (cherry picked from commit 6476e06) * Add React to package * Update component to func instead * Linter improve * Add remove file back * Improve scroll view behavior in screen * Update snapshot * Add new prop * Add justify content style only if preset is fixed * Linter fix * Add extra = symbol * Update react-native-reanimated version * Improve SectionListWithKeyboardAwareScrollViewProps implementation * PR feedback * Add fix to improve UX in Android * Improve format * Fix after merge * Remove yarn cache files * Remove unused import * Improve format * Mirror SectionList typing SectionList uses `any` by default for ItemType, and most people will specify the type of items within `renderItem`. So to keep supporting that usage, make `any` the default. --------- Co-authored-by: Mark Rickert <[email protected]> Co-authored-by: Frank Calise <[email protected]> Co-authored-by: Lizzi Lindboe <[email protected]>
1 parent 8bea1f9 commit ddfc17a

File tree

7 files changed

+129
-39
lines changed

7 files changed

+129
-39
lines changed

README.md

Lines changed: 22 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -64,27 +64,28 @@ We've put great effort into the documentation as a team, please [read through it
6464

6565
Nothing makes it into Ignite unless it's been proven on projects that Infinite Red works on. Ignite apps include the following rock-solid technical decisions out of the box:
6666

67-
| Library | Category | Version | Description |
68-
| ----------------- | -------------------- | ------- | ---------------------------------------------- |
69-
| React Native | Mobile Framework | v0.74 | The best cross-platform mobile framework |
70-
| React | UI Framework | v18 | The most popular UI framework in the world |
71-
| TypeScript | Language | v5 | Static typechecking |
72-
| React Navigation | Navigation | v6 | Performant and consistent navigation framework |
73-
| MobX-State-Tree | State Management | v5 | Observable state tree |
74-
| MobX-React-Lite | React Integration | v3 | Re-render React performantly |
75-
| Expo | SDK | v51 | Allows (optional) Expo modules |
76-
| Expo Font | Custom Fonts | v12 | Import custom fonts |
77-
| Expo Localization | Internationalization | v15 | i18n support (including RTL!) |
78-
| Expo Status Bar | Status Bar Library | v1 | Status bar support |
79-
| RN Reanimated | Animations | v3 | Beautiful and performant animations |
80-
| AsyncStorage | Persistence | v1 | State persistence |
81-
| apisauce | REST client | v2 | Communicate with back-end |
82-
| Reactotron RN | Inspector/Debugger | v3 | JS debugging |
83-
| Hermes | JS engine | | Fine-tuned JS engine for RN |
84-
| Jest | Test Runner | v26 | Standard test runner for JS apps |
85-
| Maestro | Testing Framework | | Automate end-to-end UI testing |
86-
| date-fns | Date library | v2 | Excellent date library |
87-
| FlashList | FlatList replacement | v1 | A performant drop-in replacement for FlatList |
67+
| Library | Category | Version | Description |
68+
| -------------------------------- | -------------------- | ------- | ---------------------------------------------- |
69+
| React Native | Mobile Framework | v0.74 | The best cross-platform mobile framework |
70+
| React | UI Framework | v18 | The most popular UI framework in the world |
71+
| TypeScript | Language | v5 | Static typechecking |
72+
| React Navigation | Navigation | v6 | Performant and consistent navigation framework |
73+
| MobX-State-Tree | State Management | v5 | Observable state tree |
74+
| MobX-React-Lite | React Integration | v3 | Re-render React performantly |
75+
| Expo | SDK | v51 | Allows (optional) Expo modules |
76+
| Expo Font | Custom Fonts | v12 | Import custom fonts |
77+
| Expo Localization | Internationalization | v15 | i18n support (including RTL!) |
78+
| Expo Status Bar | Status Bar Library | v1 | Status bar support |
79+
| RN Reanimated | Animations | v3 | Beautiful and performant animations |
80+
| AsyncStorage | Persistence | v1 | State persistence |
81+
| apisauce | REST client | v2 | Communicate with back-end |
82+
| Reactotron RN | Inspector/Debugger | v3 | JS debugging |
83+
| Hermes | JS engine | | Fine-tuned JS engine for RN |
84+
| Jest | Test Runner | v26 | Standard test runner for JS apps |
85+
| Maestro | Testing Framework | | Automate end-to-end UI testing |
86+
| date-fns | Date library | v2 | Excellent date library |
87+
| react-native-keyboard-controller | Keyboard library | v1 | Great keyboard manager library |
88+
| FlashList | FlatList replacement | v1 | A performant drop-in replacement for FlatList |
8889

8990
Ignite also comes with a [component library](./docs/boilerplate/app/components/Components.md) that is tuned for custom designs, theming support, testing, custom fonts, generators, and much, much more.
9091

boilerplate/app/app.tsx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import { ErrorBoundary } from "./screens/ErrorScreen/ErrorBoundary"
2929
import * as storage from "./utils/storage"
3030
import { customFontsToLoad } from "./theme"
3131
import Config from "./config"
32+
import { KeyboardProvider } from "react-native-keyboard-controller"
3233

3334
export const NAVIGATION_PERSISTENCE_KEY = "NAVIGATION_STATE"
3435

@@ -106,11 +107,13 @@ function App(props: AppProps) {
106107
return (
107108
<SafeAreaProvider initialMetrics={initialWindowMetrics}>
108109
<ErrorBoundary catchErrors={Config.catchErrors}>
109-
<AppNavigator
110-
linking={linking}
111-
initialState={initialNavigationState}
112-
onStateChange={onNavigationStateChange}
113-
/>
110+
<KeyboardProvider>
111+
<AppNavigator
112+
linking={linking}
113+
initialState={initialNavigationState}
114+
onStateChange={onNavigationStateChange}
115+
/>
116+
</KeyboardProvider>
114117
</ErrorBoundary>
115118
</SafeAreaProvider>
116119
)

boilerplate/app/components/Screen.tsx

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,11 @@ import {
1414
} from "react-native"
1515
import { $styles } from "../theme"
1616
import { ExtendedEdge, useSafeAreaInsetsStyle } from "../utils/useSafeAreaInsetsStyle"
17+
import { KeyboardAwareScrollView } from "react-native-keyboard-controller"
1718
import { useAppTheme } from "app/utils/useAppTheme"
1819

20+
export const DEFAULT_BOTTOM_OFFSET = 50
21+
1922
interface BaseScreenProps {
2023
/**
2124
* Children components.
@@ -45,6 +48,10 @@ interface BaseScreenProps {
4548
* By how much should we offset the keyboard? Defaults to 0.
4649
*/
4750
keyboardOffset?: number
51+
/**
52+
* By how much we scroll up when the keyboard is shown. Defaults to 50.
53+
*/
54+
keyboardBottomOffset?: number
4855
/**
4956
* Pass any additional props directly to the StatusBar component.
5057
*/
@@ -165,10 +172,12 @@ function useAutoPreset(props: AutoScreenProps): {
165172
* @returns {JSX.Element} - The rendered `ScreenWithoutScrolling` component.
166173
*/
167174
function ScreenWithoutScrolling(props: ScreenProps) {
168-
const { style, contentContainerStyle, children } = props
175+
const { style, contentContainerStyle, children, preset } = props
169176
return (
170177
<View style={[$outerStyle, style]}>
171-
<View style={[$innerStyle, contentContainerStyle]}>{children}</View>
178+
<View style={[$innerStyle, preset === "fixed" && $justifyFlexEnd, contentContainerStyle]}>
179+
{children}
180+
</View>
172181
</View>
173182
)
174183
}
@@ -181,6 +190,7 @@ function ScreenWithScrolling(props: ScreenProps) {
181190
const {
182191
children,
183192
keyboardShouldPersistTaps = "handled",
193+
keyboardBottomOffset = DEFAULT_BOTTOM_OFFSET,
184194
contentContainerStyle,
185195
ScrollViewProps,
186196
style,
@@ -195,7 +205,8 @@ function ScreenWithScrolling(props: ScreenProps) {
195205
useScrollToTop(ref)
196206

197207
return (
198-
<ScrollView
208+
<KeyboardAwareScrollView
209+
bottomOffset={keyboardBottomOffset}
199210
{...{ keyboardShouldPersistTaps, scrollEnabled, ref }}
200211
{...ScrollViewProps}
201212
onLayout={(e) => {
@@ -214,7 +225,7 @@ function ScreenWithScrolling(props: ScreenProps) {
214225
]}
215226
>
216227
{children}
217-
</ScrollView>
228+
</KeyboardAwareScrollView>
218229
)
219230
}
220231

@@ -283,6 +294,10 @@ const $outerStyle: ViewStyle = {
283294
width: "100%",
284295
}
285296

297+
const $justifyFlexEnd: ViewStyle = {
298+
justifyContent: "flex-end",
299+
}
300+
286301
const $innerStyle: ViewStyle = {
287302
justifyContent: "flex-start",
288303
alignItems: "stretch",

boilerplate/app/screens/DemoShowroomScreen/DemoShowroomScreen.tsx

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { $styles } from "app/theme"
1111
import { useSafeAreaInsetsStyle } from "../../utils/useSafeAreaInsetsStyle"
1212
import * as Demos from "./demos"
1313
import { DrawerIconButton } from "./DrawerIconButton"
14+
import SectionListWithKeyboardAwareScrollView from "./SectionListWithKeyboardAwareScrollView"
1415
import { useAppTheme } from "app/utils/useAppTheme"
1516

1617
const logo = require("../../../assets/images/logo.png")
@@ -80,6 +81,7 @@ const NativeListItem: FC<DemoListItem> = ({ item, sectionIndex, handleScroll })
8081
}
8182

8283
const ShowroomListItem = Platform.select({ web: WebListItem, default: NativeListItem })
84+
const isAndroid = Platform.OS === "android"
8385

8486
export const DemoShowroomScreen: FC<DemoTabScreenProps<"DemoShowroom">> =
8587
function DemoShowroomScreen(_props) {
@@ -181,22 +183,30 @@ export const DemoShowroomScreen: FC<DemoTabScreenProps<"DemoShowroom">> =
181183
</View>
182184
)}
183185
>
184-
<Screen preset="fixed" safeAreaEdges={["top"]} contentContainerStyle={$styles.flex1}>
186+
<Screen
187+
preset="fixed"
188+
safeAreaEdges={["top"]}
189+
contentContainerStyle={$styles.flex1}
190+
{...(isAndroid ? { KeyboardAvoidingViewProps: { behavior: undefined } } : {})}
191+
>
185192
<DrawerIconButton onPress={toggleDrawer} />
186193

187-
<SectionList
194+
<SectionListWithKeyboardAwareScrollView
188195
ref={listRef}
189196
contentContainerStyle={themed($sectionListContentContainer)}
190197
stickySectionHeadersEnabled={false}
191198
sections={Object.values(Demos).map((d) => ({
192-
...d,
199+
name: d.name,
200+
description: d.description,
193201
data: [d.data({ theme, themed })],
194202
}))}
195-
renderItem={({ item, index: sectionIndex }) =>
196-
item.map((demo: ReactElement, demoIndex: number) => (
197-
<View key={`${sectionIndex}-${demoIndex}`}>{demo}</View>
198-
))
199-
}
203+
renderItem={({ item, index: sectionIndex }) => (
204+
<View>
205+
{item.map((demo: ReactElement, demoIndex: number) => (
206+
<View key={`${sectionIndex}-${demoIndex}`}>{demo}</View>
207+
))}
208+
</View>
209+
)}
200210
renderSectionFooter={() => <View style={themed($demoUseCasesSpacer)} />}
201211
ListHeaderComponent={
202212
<View style={themed($heading)}>
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { DEFAULT_BOTTOM_OFFSET } from "app/components"
2+
import React, { ReactElement, useCallback } from "react"
3+
import { ScrollViewProps, SectionList, SectionListProps } from "react-native"
4+
import { KeyboardAwareScrollView } from "react-native-keyboard-controller"
5+
6+
type SectionType<ItemType> = {
7+
name: string
8+
description: string
9+
data: ItemType[]
10+
}
11+
12+
type SectionListWithKeyboardAwareScrollViewProps<ItemType> = SectionListProps<ItemType> & {
13+
/* Optional function to pass a custom scroll component */
14+
renderScrollComponent?: (props: ScrollViewProps) => React.ReactNode
15+
/* Optional additional offset between TextInput bottom edge and keyboard top edge. See https://kirillzyusko.github.io/react-native-keyboard-controller/docs/api/components/keyboard-aware-scroll-view#bottomoffset */
16+
bottomOffset?: number
17+
/* The sections to be rendered in the list */
18+
sections: SectionType<ItemType>[]
19+
/* Function to render the header for each section */
20+
renderSectionHeader: ({ section }: { section: SectionType<ItemType> }) => React.ReactNode
21+
}
22+
23+
function SectionListWithKeyboardAwareScrollView<ItemType = any>(
24+
{
25+
renderScrollComponent,
26+
bottomOffset = DEFAULT_BOTTOM_OFFSET,
27+
contentContainerStyle,
28+
...props
29+
}: SectionListWithKeyboardAwareScrollViewProps<ItemType>,
30+
ref: React.Ref<SectionList<ItemType>>,
31+
): ReactElement {
32+
const defaultRenderScrollComponent = useCallback(
33+
(props: ScrollViewProps) => (
34+
<KeyboardAwareScrollView
35+
contentContainerStyle={contentContainerStyle}
36+
bottomOffset={bottomOffset}
37+
{...props}
38+
/>
39+
),
40+
[contentContainerStyle, bottomOffset],
41+
)
42+
43+
return (
44+
<SectionList
45+
{...props}
46+
ref={ref}
47+
renderScrollComponent={renderScrollComponent ?? defaultRenderScrollComponent}
48+
/>
49+
)
50+
}
51+
52+
export default React.forwardRef(SectionListWithKeyboardAwareScrollView) as <ItemType = any>(
53+
props: SectionListWithKeyboardAwareScrollViewProps<ItemType> & {
54+
ref?: React.Ref<SectionList<ItemType>>
55+
},
56+
) => ReactElement
57+
58+
// @demo remove-file

boilerplate/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,9 @@
5757
"react-native": "0.74.5",
5858
"react-native-drawer-layout": "4.0.0-alpha.9",
5959
"react-native-gesture-handler": "~2.16.1",
60+
"react-native-keyboard-controller": "^1.12.7",
6061
"react-native-mmkv": "^2.12.2",
61-
"react-native-reanimated": "~3.10.1",
62+
"react-native-reanimated": "~3.15.0",
6263
"react-native-safe-area-context": "4.10.5",
6364
"react-native-screens": "3.31.1",
6465
"react-native-web": "~0.19.6"

test/vanilla/__snapshots__/ignite-remove-demo.test.ts.snap

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ removing file /user/home/ignite/app/screens/DemoShowroomScreen/DemoDivider.tsx
2222
removing file /user/home/ignite/app/screens/DemoShowroomScreen/DemoShowroomScreen.tsx
2323
removing file /user/home/ignite/app/screens/DemoShowroomScreen/DemoUseCase.tsx
2424
removing file /user/home/ignite/app/screens/DemoShowroomScreen/DrawerIconButton.tsx
25+
removing file /user/home/ignite/app/screens/DemoShowroomScreen/SectionListWithKeyboardAwareScrollView.tsx
2526
removing file /user/home/ignite/app/screens/DemoShowroomScreen/demos/DemoAutoImage.tsx
2627
removing file /user/home/ignite/app/screens/DemoShowroomScreen/demos/DemoButton.tsx
2728
removing file /user/home/ignite/app/screens/DemoShowroomScreen/demos/DemoCard.tsx
@@ -72,6 +73,7 @@ removing file /user/home/ignite/app/screens/LoginScreen.tsx
7273
Found 'remove-file' in /user/home/ignite/app/screens/DemoShowroomScreen/DemoShowroomScreen.tsx
7374
Found 'remove-file' in /user/home/ignite/app/screens/DemoShowroomScreen/DemoUseCase.tsx
7475
Found 'remove-file' in /user/home/ignite/app/screens/DemoShowroomScreen/DrawerIconButton.tsx
76+
Found 'remove-file' in /user/home/ignite/app/screens/DemoShowroomScreen/SectionListWithKeyboardAwareScrollView.tsx
7577
Found '@demo remove-block-start', '@demo remove-block-end' in /user/home/ignite/app/screens/index.ts
7678
Found 'remove-file' in /user/home/ignite/app/screens/LoginScreen.tsx
7779
Found '@demo remove-current-line', '@demo remove-block-start', '@demo remove-block-end' in /user/home/ignite/app/screens/WelcomeScreen.tsx

0 commit comments

Comments
 (0)