Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions formulus-formplayer/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -514,7 +514,7 @@ function App() {
// Render loading state or error if needed
if (isLoading) {
return (
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', height: '100vh' }}>
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', height: '100dvh' }}>
<CircularProgress />
<Typography variant="h6" sx={{ mt: 2 }}>
Loading form...
Expand All @@ -528,7 +528,7 @@ function App() {

if (loadError || !schema || !uischema) {
return (
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', height: '100vh' }}>
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', height: '100dvh' }}>
<Typography variant="h6" color="error">
{loadError || 'Failed to load form'}
</Typography>
Expand Down Expand Up @@ -562,7 +562,7 @@ function App() {
className="App"
style={{
display: 'flex',
height: '100vh',
height: '100dvh', // Use dynamic viewport height for mobile keyboard support
width: '100%'
}}
>
Expand Down
184 changes: 184 additions & 0 deletions formulus-formplayer/src/FormLayout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import React, { ReactNode } from 'react';
import { Box, Paper, Stack, Button } from '@mui/material';

interface FormLayoutProps {
/**
* The main form content to display
*/
children: ReactNode;

/**
* Previous button configuration
*/
previousButton?: {
label?: string;
onClick: () => void;
disabled?: boolean;
};

/**
* Next button configuration
*/
nextButton?: {
label?: string;
onClick: () => void;
disabled?: boolean;
};

/**
* Optional header content (e.g., progress bar)
*/
header?: ReactNode;

/**
* Additional padding at the bottom of content area (in pixels)
* Default: 120px to ensure content is never hidden behind navigation
*/
contentBottomPadding?: number;

/**
* Whether to show navigation buttons
* Default: true
*/
showNavigation?: boolean;
}

/**
* FormLayout Component
*
* A robust, responsive layout component for forms that:
* - Prevents navigation buttons from overlapping form content
* - Handles mobile keyboard appearance correctly
* - Ensures all form fields are scrollable and accessible
* - Uses dynamic viewport height (100dvh) for proper mobile support
*
* Layout Structure:
* - Header area (sticky at top, optional)
* - Scrollable content area (flexible, with bottom padding)
* - Navigation bar (sticky at bottom, non-overlapping)
*/
const FormLayout: React.FC<FormLayoutProps> = ({
children,
previousButton,
nextButton,
header,
contentBottomPadding = 120,
showNavigation = true
}) => {
return (
<Box
sx={{
display: 'flex',
flexDirection: 'column',
height: '100dvh', // Dynamic viewport height - adapts to mobile keyboard
width: '100%',
overflow: 'hidden',
position: 'relative',
// Ensure proper rendering on all devices
WebkitOverflowScrolling: 'touch',
}}
>
{/* Header Area - Sticky at top (optional) */}
{header && (
<Box
sx={{
flexShrink: 0,
position: 'sticky',
top: 0,
zIndex: 10,
backgroundColor: 'background.default',
// Add subtle shadow for visual separation
boxShadow: '0 2px 4px rgba(0,0,0,0.05)',
}}
>
{header}
</Box>
)}

{/* Scrollable Content Area */}
<Box
sx={{
flex: 1,
overflowY: 'auto',
overflowX: 'hidden',
// Use -webkit-overflow-scrolling for smooth iOS scrolling
WebkitOverflowScrolling: 'touch',
// Add padding to prevent content from being hidden behind navigation
paddingBottom: `${contentBottomPadding}px`,
// Ensure proper scrolling behavior when keyboard appears
overscrollBehavior: 'contain',
}}
>
{children}
</Box>

{/* Navigation Bar - Sticky at bottom, non-overlapping */}
{showNavigation && (previousButton || nextButton) && (
<Paper
elevation={3}
sx={{
flexShrink: 0,
position: 'sticky',
bottom: 0,
zIndex: 10,
width: '100%',
padding: { xs: 1.5, sm: 2 },
backgroundColor: 'background.paper',
// Add border-top for visual separation
borderTop: '1px solid',
borderColor: 'divider',
// Ensure it stays above content
boxShadow: '0 -2px 8px rgba(0,0,0,0.1)',
}}
>
<Stack
direction="row"
spacing={2}
justifyContent="center"
sx={{
// Full-width buttons on mobile, auto-width on larger screens
'& > *': {
flex: { xs: 1, sm: '0 1 auto' },
minWidth: { xs: 'auto', sm: '120px' },
},
}}
>
{previousButton && (
<Button
variant="contained"
onClick={previousButton.onClick}
disabled={previousButton.disabled}
fullWidth={false}
sx={{
// Ensure buttons are touch-friendly
minHeight: { xs: '44px', sm: '36px' }, // iOS recommended touch target
fontSize: { xs: '1rem', sm: '0.875rem' },
}}
>
{previousButton.label || 'Previous'}
</Button>
)}
{nextButton && (
<Button
variant="contained"
onClick={nextButton.onClick}
disabled={nextButton.disabled}
fullWidth={false}
sx={{
// Ensure buttons are touch-friendly
minHeight: { xs: '44px', sm: '36px' }, // iOS recommended touch target
fontSize: { xs: '1rem', sm: '0.875rem' },
}}
>
{nextButton.label || 'Next'}
</Button>
)}
</Stack>
</Paper>
)}
</Box>
);
};

export default FormLayout;

70 changes: 33 additions & 37 deletions formulus-formplayer/src/SwipeLayoutRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import React, { useCallback, useState, useEffect, useMemo } from "react";
import { JsonFormsDispatch, withJsonFormsControlProps } from "@jsonforms/react";
import { ControlProps, rankWith, uiTypeIs, RankedTester } from "@jsonforms/core";
import { useSwipeable } from "react-swipeable";
import { Button, Box } from "@mui/material";
import { useFormContext } from "./App";
import { draftService } from './DraftService';
import FormProgressBar from './FormProgressBar';
import FormLayout from './FormLayout';

interface SwipeLayoutProps extends ControlProps {
currentPage: number;
Expand Down Expand Up @@ -89,17 +89,37 @@ const SwipeLayoutRenderer = ({
}, [layouts, currentPage]);

return (
<Box sx={{ position: 'relative', height: '100%' }}>
{/* Progress Bar */}
<FormProgressBar
currentPage={currentPage}
totalScreens={totalScreens}
data={data}
schema={schema}
uischema={uischema}
mode="screens"
isOnFinalizePage={isOnFinalizePage}
/>
<FormLayout
header={
<FormProgressBar
currentPage={currentPage}
totalScreens={totalScreens}
data={data}
schema={schema}
uischema={uischema}
mode="screens"
isOnFinalizePage={isOnFinalizePage}
/>
}
previousButton={
currentPage > 0
? {
onClick: () => navigateToPage(Math.max(currentPage - 1, 0)),
disabled: isNavigating,
}
: undefined
}
nextButton={
currentPage < layouts.length - 1
? {
onClick: () => navigateToPage(Math.min(currentPage + 1, layouts.length - 1)),
disabled: isNavigating,
}
: undefined
}
contentBottomPadding={120}
showNavigation={true}
>
<div {...handlers} className="swipelayout_screen">
{(uischema as any)?.label && <h1>{(uischema as any).label}</h1>}
{layouts.length > 0 && (
Expand All @@ -113,31 +133,7 @@ const SwipeLayoutRenderer = ({
/>
)}
</div>
<Box sx={{
position: "absolute",
bottom: 20,
width: "100%",
textAlign: "center",
display: 'flex',
justifyContent: 'center',
gap: 2
}}>
<Button
variant="contained"
onClick={() => navigateToPage(Math.max(currentPage - 1, 0))}
disabled={currentPage === 0 || isNavigating}
>
Previous
</Button>
<Button
variant="contained"
onClick={() => navigateToPage(Math.min(currentPage + 1, layouts.length - 1))}
disabled={currentPage === layouts.length - 1 || isNavigating}
>
Next
</Button>
</Box>
</Box>
</FormLayout>
);
};

Expand Down
9 changes: 5 additions & 4 deletions formulus-formplayer/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,14 @@ body {

.swipelayout_screen {
margin: 0;
padding-top: 4vh;
padding-top: 2vh;
padding-left: 2%;
padding-right: 2%;
padding-bottom: 0;
padding-bottom: 2vh;
width: 96%;
height: 96vh;
overflow: hidden;
/* Remove fixed height and overflow hidden - let FormLayout handle scrolling */
min-height: 100%;
box-sizing: border-box;
}

.swipelayout_screen > div > div {
Expand Down