Skip to content

Commit 330d3ef

Browse files
feat(core): implement advanced keyboard management and fix monorepo b… (#4)
* feat(core): implement advanced keyboard management and fix monorepo bundling This commit introduces several new props to FieldForm (autoScroll, chainEnabled, autoReturnKeyType, dismissKeyboardOnTap, etc.) and resolves bundling issues in the monorepo showcase app by hoisting runtime polyfills to the project root. * feat: add field skipping, Reanimated keyboard support, and optional submit-on-done behavior to FieldForm * docs: update README with performance details, default padding, and new field skip/submit props * feat: implement high-performance keyboard accessory with professional-grade animations and automatic bottom offset handling * feat(example): introduce comprehensive 11-screen demo showcase suite This commit introduces a robust, interactive 11-screen demo suite to the react-native-fieldflow example application. The showcase is designed to demonstrate core features, advanced layouts, hook behaviors, and native parity with high-quality, professional SaaS-oriented visuals. Changes include: - Core Basics: Setup of fundamental usage (01-login, 02-signup, 03-checkout, 04-profile). Demonstrating zero-config fields, form chaining, and dynamic skipping. - Hook & Behavior Demos: Implemented custom floating buttons, collapsing headers via useKeyboardVisible, and varying submit behaviors (05-floating-button, 06-collapsing-header, 07-submit-behavior). - Advanced Layouts: Integrated examples for long scrollable forms, standard React Navigation setups, Before/After LoC comparisons, and native-feeling keyboard accessory views (08-long-form, 09-react-navigation, 10-comparison, 11-accessory-view). - Infrastructure: Added a dedicated sub-stack (demos/_layout.tsx) wired into (tabs)/index.tsx and app/_layout.tsx enforcing a consistent Light Mode theme across all showcases. - UI Kit: Introduced an IconButton component to standardize platform-agnostic circular touch targets and resolved icon/cropping lint issues across forms.
1 parent 90a0c48 commit 330d3ef

20 files changed

Lines changed: 3600 additions & 343 deletions

README.md

Lines changed: 74 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,19 @@ import { FieldForm, FieldInput } from 'react-native-fieldflow';
157157

158158
**FieldFlow** subscribes to native keyboard frame events. As the keyboard animates in, an internal `Animated.View` spacer at the bottom of the scroll content grows to match — pushing content up in sync with the keyboard, with no layout recalculation and no white flash.
159159

160+
### 📱 Example App
161+
162+
The included example app (built with Expo Router) contains a **full demo suite** with 11 professional-grade screens demonstrating:
163+
- **Core Basics**: Login, Multi-field chains, Checkouts with dynamic skipping.
164+
- **Hooks & Events**: Custom UI lifting with `useKeyboardHeight`, collapsing headers with `useKeyboardVisible`.
165+
- **Advanced Layouts**: Long forms with `RefreshControl`, zero-config React Navigation integration, and rich `keyboardAccessoryView` toolbars.
166+
167+
To run it:
168+
```bash
169+
cd example
170+
npx expo start
171+
```
172+
160173
At the same time, every `FieldInput` registers itself into an ordered focus chain. When you tap Next, **FieldFlow** calls `focus()` on the next ref and runs `scrollResponderScrollNativeHandleToKeyboard` to ensure the newly focused field is visible above the keyboard — even accounting for `extraScrollPadding` so it doesn't sit flush against it.
161174

162175
Nothing about this requires native modules. It is entirely JS-side and works on Expo, bare RN, and the New Architecture.
@@ -173,13 +186,19 @@ Nothing about this requires native modules. It is entirely JS-side and works on
173186
| Prop | Type | Default | Description |
174187
|------|------|---------|-------------|
175188
| `onSubmit` | `() => void` || Called when the last field is submitted |
176-
| `extraScrollPadding` | `number` | `40` | Space between the active field and the keyboard top edge |
189+
| `extraScrollPadding` | `number` | `50` | Space between the active field and the keyboard top edge |
177190
| `scrollable` | `boolean` | `true` | Wrap children in a managed ScrollView |
178191
| `avoidKeyboard` | `boolean` | `true` | Enable the animated keyboard spacer |
179-
| `submitOnLastFieldDone` | `boolean` | `false` | Submit the whole form / call `onSubmit` when "Done" is pressed on the final field |
180-
| `style` | `ViewStyle` || Container style |
192+
| `keyboardAccessoryView` | `ReactNode` || Cross-platform view that hovers above the keyboard on both iOS and Android, animated in sync with the keyboard |
193+
| `keyboardAccessoryViewMode` | `'always' \| 'whenKeyboardOpen'` | `'always'` | `'always'` — bar stays on screen at all times and lifts up; `'whenKeyboardOpen'` — bar is hidden when keyboard is closed, appears and lifts in sync when keyboard opens |
194+
| `autoScroll` | `boolean` | `true` | Automatically scroll to the focused field |
195+
| `chainEnabled` | `boolean` | `true` | Auto-focus next field on Next / Done |
196+
| `autoReturnKeyType` | `boolean` | `true` | Automatically set `returnKeyType` to `next` / `done` |
197+
| `dismissKeyboardOnTap` | `boolean` | `false` | Tapping outside any input dismisses the keyboard |
198+
| `submitOnLastFieldDone` | `boolean` | `false` | Submit when Done is pressed on the final field |
181199
| `scrollViewProps` | `ScrollViewProps` || Forwarded to the internal ScrollView |
182-
| `onKeyboardShow` | `(height: number) => void` || Called when keyboard appears |
200+
| `keyboardVerticalOffset` | `number \| (platform) => number` | `0 / 25` | Static offset or per-platform resolver |
201+
| `onKeyboardShow` | `(payload) => void` || Called when keyboard appears |
183202
| `onKeyboardHide` | `() => void` || Called when keyboard dismisses |
184203

185204
### `<FieldInput>`
@@ -192,6 +211,57 @@ Accepts every `TextInput` prop, plus:
192211
| `nextRef` | `RefObject<TextInput>` || Override: focus this ref instead of the auto-detected next field |
193212
| `onFormSubmit` | `() => void` || Override: called when this field is the last and Done is tapped |
194213

214+
---
215+
216+
## Keyboard Accessory View
217+
218+
`keyboardAccessoryView` renders a cross-platform toolbar that floats above the keyboard and animates in **perfect sync** with it on both iOS and Android.
219+
220+
**Professional-grade animations:**
221+
- **Show**: Uses a custom **exponential ease-out** curve that frontloads the motion, ensuring the bar settles exactly when the keyboard spring does — no trailing or "pushed" movement.
222+
- **Hide (Always)**: When the keyboard closes in `always` mode, the bar uses a **spring with a subtle bounce** to settle back at the bottom, creating a natural gravity-drop feel.
223+
- **Hide (WhenOpen)**: Stays timed with the keyboard's own dismiss animation for a coordinated exit.
224+
- **Auto-Offset**: The component automatically detects if it's inside a Tab Bar or has other bottom insets and subtracts them from the keyboard height, so the bar always docks flush with the top of the keyboard.
225+
226+
The accessory height is measured automatically and injected as `paddingBottom` into the `ScrollView` so that the last field is always scrollable above the bar.
227+
228+
```tsx
229+
<FieldForm
230+
keyboardAccessoryView={
231+
<View style={styles.toolbar}>
232+
<TouchableOpacity onPress={Keyboard.dismiss}>
233+
<Text>Done</Text>
234+
</TouchableOpacity>
235+
</View>
236+
}
237+
>
238+
<FieldInput placeholder="Message..." />
239+
</FieldForm>
240+
```
241+
242+
### `keyboardAccessoryViewMode`
243+
244+
| Value | Behaviour |
245+
|-------|-----------|
246+
| `'always'` *(default)* | Bar always visible at the bottom; slides up when keyboard opens and back down when it closes |
247+
| `'whenKeyboardOpen'` | Bar is hidden when no keyboard is shown; fades in and slides up as keyboard opens; fades out and slides down as keyboard closes |
248+
249+
```tsx
250+
// Bar is always visible — use for persistent action bars / send buttons
251+
<FieldForm
252+
keyboardAccessoryView={<Toolbar />}
253+
keyboardAccessoryViewMode="always"
254+
/>
255+
256+
// Bar only appears with the keyboard — use for formatting toolbars
257+
<FieldForm
258+
keyboardAccessoryView={<Toolbar />}
259+
keyboardAccessoryViewMode="whenKeyboardOpen"
260+
/>
261+
```
262+
263+
---
264+
195265
### Hooks
196266

197267
```tsx

example/app/(tabs)/hooks.tsx

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ export default function HooksScreen() {
4747
const [showVisible, setShowVisible] = useState(true);
4848
const [showState, setShowState] = useState(true);
4949
const [showSubscribe, setShowSubscribe] = useState(true);
50+
const [accessoryMode, setAccessoryMode] = useState<'always' | 'whenKeyboardOpen'>('whenKeyboardOpen');
5051

5152
// ── Imperative subscription ─────────────────────────
5253
const [events, setEvents] = useState<string[]>([]);
@@ -88,8 +89,18 @@ export default function HooksScreen() {
8889
return (
8990
<View style={[styles.root, { paddingTop: insets.top }]}>
9091
<FieldForm
91-
extraScrollPadding={100}
92+
extraScrollPadding={0}
9293
keyboardVerticalOffset={0}
94+
keyboardAccessoryViewMode={accessoryMode}
95+
keyboardAccessoryView={
96+
<View style={styles.accessoryBar}>
97+
<Ionicons name={visible ? 'close-circle-outline' : 'keyboard-outline'} size={18} color="#fff" />
98+
<Text style={styles.accessoryText}>
99+
{visible ? 'Keyboard open · tap to dismiss' : 'Tap a field to open keyboard'}
100+
</Text>
101+
<Text style={styles.accessoryMode}>{accessoryMode}</Text>
102+
</View>
103+
}
93104
scrollViewProps={{
94105
contentContainerStyle: styles.scrollContent,
95106
keyboardDismissMode: 'interactive',
@@ -138,6 +149,21 @@ export default function HooksScreen() {
138149
</View>
139150
</View>
140151

152+
{/* ── Accessory View Mode ───────────────────── */}
153+
<View style={styles.section}>
154+
<SectionLabel
155+
title="Keyboard Accessory"
156+
description="Controls when the bar above the keyboard appears"
157+
/>
158+
<FeatureCard
159+
icon="browsers-outline"
160+
title="Always visible"
161+
description={accessoryMode === 'always' ? 'Bar stays on screen at all times' : 'Bar only shows with keyboard'}
162+
toggleValue={accessoryMode === 'always'}
163+
onToggle={(v) => setAccessoryMode(v ? 'always' : 'whenKeyboardOpen')}
164+
/>
165+
</View>
166+
141167
{/* ── Metrics ──────────────────────────────── */}
142168
<View style={styles.section}>
143169
<SectionLabel title="Live Values" />
@@ -271,6 +297,28 @@ const styles = StyleSheet.create({
271297
// marginBottom spacer now handled by KeyboardForm
272298
},
273299

300+
// Accessory bar
301+
accessoryBar: {
302+
flexDirection: 'row',
303+
alignItems: 'center',
304+
backgroundColor: C.accent,
305+
paddingHorizontal: ShowcaseSpacing.lg,
306+
paddingVertical: ShowcaseSpacing.md,
307+
gap: ShowcaseSpacing.sm,
308+
},
309+
accessoryText: {
310+
flex: 1,
311+
fontSize: 14,
312+
fontWeight: '600',
313+
color: '#fff',
314+
},
315+
accessoryMode: {
316+
fontSize: 11,
317+
fontWeight: '500',
318+
color: 'rgba(255,255,255,0.7)',
319+
fontVariant: ['tabular-nums' as const],
320+
},
321+
274322
header: {
275323
paddingHorizontal: ShowcaseSpacing.xl,
276324
paddingTop: ShowcaseSpacing.xxl,

example/app/(tabs)/index.tsx

Lines changed: 92 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -82,23 +82,102 @@ export default function HomeScreen() {
8282
</Text>
8383
</View>
8484

85-
{/* ── Live Status ──────────────────────────── */}
86-
<View style={styles.statusRow}>
87-
<StatusPill
88-
label="Keyboard"
89-
value={kbVisible ? 'Open' : 'Closed'}
90-
active={kbVisible}
85+
{/* ── Demo Categories ──────────────────────── */}
86+
<View style={styles.section}>
87+
<SectionLabel
88+
title="Core Basics"
89+
description="The essentials of FieldFlow: chaining and avoidance"
90+
/>
91+
<View style={styles.navStack}>
92+
<FeatureCard
93+
icon="log-in-outline"
94+
title="01. Login (Hello World)"
95+
description="Simplest possible form — 2 fields, no config"
96+
onPress={() => router.push('/demos/01-login')}
97+
/>
98+
<FeatureCard
99+
icon="person-add-outline"
100+
title="02. Sign Up"
101+
description="5-field chain with focus visualizer"
102+
onPress={() => router.push('/demos/02-signup')}
103+
/>
104+
<FeatureCard
105+
icon="cart-outline"
106+
title="03. Checkout"
107+
description="Dynamic field skipping with skip prop"
108+
onPress={() => router.push('/demos/03-checkout')}
109+
/>
110+
<FeatureCard
111+
icon="create-outline"
112+
title="04. Profile Edit"
113+
description="nextRef override & two-column layout"
114+
onPress={() => router.push('/demos/04-profile')}
115+
/>
116+
</View>
117+
</View>
118+
119+
<View style={styles.section}>
120+
<SectionLabel
121+
title="Hooks & Behavior"
122+
description="Lifting UI and responding to keyboard state"
91123
/>
92-
<StatusPill
93-
label="Height"
94-
value={`${Math.round(kbHeight)}px`}
95-
active={kbHeight > 0}
124+
<View style={styles.navStack}>
125+
<FeatureCard
126+
icon="move-outline"
127+
title="05. Floating Button"
128+
description="Lift UI using useKeyboardHeight"
129+
onPress={() => router.push('/demos/05-floating-button')}
130+
/>
131+
<FeatureCard
132+
icon="browsers-outline"
133+
title="06. Collapsing Header"
134+
description="Animate UI out using useKeyboardVisible"
135+
onPress={() => router.push('/demos/06-collapsing-header')}
136+
/>
137+
<FeatureCard
138+
icon="checkmark-done-outline"
139+
title="07. Submit Behavior"
140+
description="submitOnLastFieldDone contrast demo"
141+
onPress={() => router.push('/demos/07-submit-behavior')}
142+
/>
143+
</View>
144+
</View>
145+
146+
<View style={styles.section}>
147+
<SectionLabel
148+
title="Advanced Layouts"
149+
description="Deep customisation and platform parity"
96150
/>
151+
<View style={styles.navStack}>
152+
<FeatureCard
153+
icon="list-outline"
154+
title="08. Long Form / Scroll"
155+
description="scrollViewProps & RefreshControl"
156+
onPress={() => router.push('/demos/08-long-form')}
157+
/>
158+
<FeatureCard
159+
icon="navigate-outline"
160+
title="09. React Navigation"
161+
description="Automatic window height measurement"
162+
onPress={() => router.push('/demos/09-react-navigation')}
163+
/>
164+
<FeatureCard
165+
icon="git-compare-outline"
166+
title="10. Before / After"
167+
description="FieldFlow vs Boilerplate (LoC saved)"
168+
onPress={() => router.push('/demos/10-comparison')}
169+
/>
170+
<FeatureCard
171+
icon="chatbox-ellipses-outline"
172+
title="11. Accessory View"
173+
description="Chat composer with accessory toolbar"
174+
onPress={() => router.push('/demos/11-accessory-view')}
175+
/>
176+
</View>
97177
</View>
98178

99-
{/* ── Feature Toggles ──────────────────────── */}
100179
<View style={styles.section}>
101-
<SectionLabel title="Features" description="Toggle to preview behaviour" />
180+
<SectionLabel title="Live Status" />
102181
<View style={styles.toggleList}>
103182
<FeatureCard
104183
icon="resize-outline"
@@ -141,7 +220,7 @@ export default function HomeScreen() {
141220
{/* ── Quick Demo Form ──────────────────────── */}
142221
<View style={styles.section}>
143222
<SectionLabel
144-
title="Quick Demo"
223+
title="Quick Test"
145224
description="Tap Next to chain focus, Done to submit"
146225
/>
147226
<View style={styles.card}>
@@ -172,34 +251,6 @@ export default function HomeScreen() {
172251
</View>
173252
</View>
174253

175-
{/* ── Separator ────────────────────────────── */}
176-
<View style={styles.separator} />
177-
178-
{/* ── Screen Navigation ────────────────────── */}
179-
<View style={styles.section}>
180-
<SectionLabel title="Explore" />
181-
<View style={styles.navStack}>
182-
<FeatureCard
183-
icon="document-text-outline"
184-
title="Login Form"
185-
description="Chained inputs with Next / Done"
186-
onPress={() => router.push('/(tabs)/login')}
187-
/>
188-
<FeatureCard
189-
icon="pulse-outline"
190-
title="Hooks & State"
191-
description="useKeyboardHeight, useKeyboardState"
192-
onPress={() => router.push('/(tabs)/hooks')}
193-
/>
194-
<FeatureCard
195-
icon="settings-outline"
196-
title="Advanced"
197-
description="Imperative ref, callbacks, nextRef"
198-
onPress={() => router.push('/(tabs)/advanced')}
199-
/>
200-
</View>
201-
</View>
202-
203254
{/* ── API Surface ──────────────────────────── */}
204255
<View style={styles.section}>
205256
<SectionLabel title="Exports" />

example/app/_layout.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export default function RootLayout() {
1616
<ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
1717
<Stack>
1818
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
19+
<Stack.Screen name="demos" options={{ headerShown: false }} />
1920
<Stack.Screen name="modal" options={{ presentation: 'modal', title: 'Modal' }} />
2021
</Stack>
2122
<StatusBar style="auto" />

0 commit comments

Comments
 (0)