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
24 changes: 24 additions & 0 deletions demo/src/screens/componentScreens/ScreenFooterScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
ScreenFooterLayouts,
ScreenFooterBackgrounds,
KeyboardBehavior,
ScreenFooterAnimationTypeProp,
FooterAlignment,
HorizontalItemsDistribution,
ItemsFit,
Expand Down Expand Up @@ -88,6 +89,12 @@ const KEYBOARD_BEHAVIOR_OPTIONS = [
{label: 'Hoisted', value: KeyboardBehavior.HOISTED}
];

const ANIMATION_TYPE_OPTIONS = [
{label: 'Slide', value: 'slide'},
{label: 'Fade', value: 'fade'},
{label: 'None', value: 'none'}
];

const KEYBOARD_BEHAVIOR_OPTIONS_SPACED = [
{label: 'Sticky', value: KeyboardBehavior.STICKY},
{label: 'Hoisted', value: KeyboardBehavior.HOISTED},
Expand Down Expand Up @@ -115,6 +122,7 @@ const ScreenFooterContent = () => {
const [layout, setLayout] = useState<ScreenFooterLayouts>(ScreenFooterLayouts.HORIZONTAL);
const [background, setBackground] = useState<ScreenFooterBackgrounds>(ScreenFooterBackgrounds.SOLID);
const [keyboardBehavior, setKeyboardBehavior] = useState<KeyboardBehavior>(KeyboardBehavior.STICKY);
const [animationType, setAnimationType] = useState<ScreenFooterAnimationTypeProp>('slide');
const [alignment, setAlignment] = useState<FooterAlignment>(FooterAlignment.CENTER);
const [horizontalAlignment, setHorizontalAlignment] = useState<FooterAlignment>(FooterAlignment.CENTER);
const [distribution, setDistribution] = useState<HorizontalItemsDistribution>(HorizontalItemsDistribution.STACK);
Expand Down Expand Up @@ -390,6 +398,21 @@ const ScreenFooterContent = () => {
</View>
</View>

{/* Animation type */}
<View marginB-s4>
<Text text70M marginB-s2>
Animation Type
</Text>
<View row>
<SegmentedControl
segments={ANIMATION_TYPE_OPTIONS}
initialIndex={ANIMATION_TYPE_OPTIONS.findIndex(opt => opt.value === animationType)}
onChangeIndex={index => setAnimationType(ANIMATION_TYPE_OPTIONS[index].value)}
/>
<View flex />
</View>
Comment on lines +412 to +413
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is minor, but you can remove these two Views and pass containerStyle={styles.rowContainer} to the SegmentedControl instead

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I copy-pasted the other parts, technically it should be using ExampleScreenPresenter all over this

</View>

{/* Alignment (Cross Axis) */}
<View marginB-s4>
<Text text70M marginB-s2>
Expand Down Expand Up @@ -478,6 +501,7 @@ const ScreenFooterContent = () => {
layout={layout}
backgroundType={background}
keyboardBehavior={keyboardBehavior}
animationType={animationType}
alignment={alignment}
horizontalAlignment={horizontalAlignment}
horizontalItemsDistribution={distribution}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export enum FloatingButtonLayouts {
HORIZONTAL = 'Horizontal'
}

export interface FloatingButtonProps extends Pick<ScreenFooterProps, 'isAndroidEdgeToEdge'> {
export interface FloatingButtonProps extends Pick<ScreenFooterProps, 'isAndroidEdgeToEdge' | 'animationType'> {
/**
* Whether the button is visible
*/
Expand Down Expand Up @@ -82,6 +82,7 @@ const FloatingButton = (props: FloatingButtonProps) => {
hideBackgroundOverlay,
hoisted = Constants.isAndroid,
isAndroidEdgeToEdge,
animationType,
testID
} = props;

Expand Down Expand Up @@ -162,6 +163,7 @@ const FloatingButton = (props: FloatingButtonProps) => {
keyboardBehavior={hoisted ? KeyboardBehavior.HOISTED : KeyboardBehavior.STICKY}
isAndroidEdgeToEdge={isAndroidEdgeToEdge}
animationDuration={withoutAnimation ? 0 : duration}
animationType={animationType}
itemsFit={fullWidth ? ItemsFit.STRETCH : undefined}
contentContainerStyle={footerContentContainerStyle}
testID={testID}
Expand Down
132 changes: 51 additions & 81 deletions packages/react-native-ui-lib/src/components/screenFooter/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, {useCallback, useEffect, useMemo, useState} from 'react';
import React, {useCallback, useMemo} from 'react';
import {LayoutChangeEvent, StyleSheet, ViewStyle} from 'react-native';
import Animated, {useAnimatedKeyboard, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated';
import Animated from 'react-native-reanimated';
import {Keyboard} from 'uilib-native';
import {SafeAreaContextPackage} from '../../optionalDependencies';
import View from '../view';
Expand All @@ -9,6 +9,7 @@ import Assets from '../../assets';
import {Colors, Shadows, Spacings} from '../../style';
import {asBaseComponent, Constants} from '../../commons/new';
import {useKeyboardHeight} from '../../hooks';
import useAnimatedFooterStyle from './useAnimatedFooterStyle';
import {
ScreenFooterProps,
ScreenFooterLayouts,
Expand All @@ -17,6 +18,7 @@ import {
HorizontalItemsDistribution,
ItemsFit,
KeyboardBehavior,
ScreenFooterAnimationTypeProp,
ScreenFooterShadow
} from './types';

Expand All @@ -28,9 +30,9 @@ export {
HorizontalItemsDistribution,
ItemsFit,
KeyboardBehavior,
ScreenFooterAnimationTypeProp,
ScreenFooterShadow
};
const androidVersion = Constants.getAndroidVersion();
const ScreenFooter = (props: ScreenFooterProps) => {
const {
testID,
Expand All @@ -44,41 +46,22 @@ const ScreenFooter = (props: ScreenFooterProps) => {
itemWidth,
horizontalItemsDistribution: distribution,
visible = true,
animationDuration = 200,
animationDuration,
animationType,
shadow = ScreenFooterShadow.SH20,
hideDivider = false,
isAndroidEdgeToEdge = !!androidVersion && androidVersion >= 35 ? true : undefined,
isAndroidEdgeToEdge,
containerStyle: containerStyleOverride,
contentContainerStyle: contentContainerStyleOverride
} = props;

const withoutAnimation = animationDuration === 0;

const keyboard = useAnimatedKeyboard({
isNavigationBarTranslucentAndroid: isAndroidEdgeToEdge,
isStatusBarTranslucentAndroid: isAndroidEdgeToEdge
});
const [height, setHeight] = useState(0);
const visibilityTranslateY = useSharedValue(0);

// Update visibility translation when visible or height changes
useEffect(() => {
visibilityTranslateY.value = withTiming(visible ? 0 : height, {duration: animationDuration});
}, [visible, height, animationDuration, visibilityTranslateY]);

// Animated style for STICKY behavior (counters Android system offset + visibility)
const stickyAnimatedStyle = useAnimatedStyle(() => {
const counterSystemOffset = Constants.isAndroid ? keyboard.height.value : 0;
return {
transform: [{translateY: counterSystemOffset + visibilityTranslateY.value}]
};
});

// Animated style for HOISTED behavior (visibility only, keyboard handled by KeyboardAccessoryView)
const hoistedAnimatedStyle = useAnimatedStyle(() => {
return {
transform: [{translateY: visibilityTranslateY.value}]
};
const {containerStyle, setHeight} = useAnimatedFooterStyle({
animationDuration,
animationType,
keyboardBehavior,
visible,
isAndroidEdgeToEdge,
containerStyle: containerStyleOverride
});

const onLayout = useCallback((event: LayoutChangeEvent) => {
Expand Down Expand Up @@ -202,30 +185,28 @@ const ScreenFooter = (props: ScreenFooterProps) => {
return null;
}, [testID, isSolid, isFading, solidBackgroundStyle]);

const renderChild = useCallback(
(child: React.ReactNode, index: number) => {
if (itemsFit === ItemsFit.FIXED && itemWidth) {
const fixedStyle: ViewStyle = isHorizontal
? {width: itemWidth, flexShrink: 1, overflow: 'hidden', flexDirection: 'row', justifyContent: 'center'}
: {width: itemWidth, maxWidth: '100%'};
return (
<View key={index} style={fixedStyle}>
{child}
</View>
);
}
const renderChild = useCallback((child: React.ReactNode, index: number) => {
if (itemsFit === ItemsFit.FIXED && itemWidth) {
const fixedStyle: ViewStyle = isHorizontal
? {width: itemWidth, flexShrink: 1, overflow: 'hidden', flexDirection: 'row', justifyContent: 'center'}
: {width: itemWidth, maxWidth: '100%'};
return (
<View key={index} style={fixedStyle}>
{child}
</View>
);
}

if (isHorizontal && React.isValidElement(child) && itemsFit === ItemsFit.STRETCH) {
return (
<View flex row centerH key={index}>
{child}
</View>
);
}
return child;
},
[itemsFit, itemWidth, isHorizontal]
);
if (isHorizontal && React.isValidElement(child) && itemsFit === ItemsFit.STRETCH) {
return (
<View flex row centerH key={index}>
{child}
</View>
);
}
return child;
},
[itemsFit, itemWidth, isHorizontal]);

const childrenArray = React.Children.toArray(children).slice(0, 3).map(renderChild);

Expand All @@ -240,20 +221,9 @@ const ScreenFooter = (props: ScreenFooterProps) => {
);
}, [renderBackground, testID, contentContainerStyle, childrenArray]);

const Container = useMemo(() => {
return withoutAnimation ? View : Animated.View;
}, [withoutAnimation]);

const containerStyle = useMemo(() => {
return withoutAnimation
? [styles.container, containerStyleOverride]
: [styles.container, hoistedAnimatedStyle, containerStyleOverride];
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [withoutAnimation, containerStyleOverride]);

if (keyboardBehavior === KeyboardBehavior.HOISTED) {
return (
<Container style={containerStyle} pointerEvents={visible ? 'box-none' : 'none'}>
const renderKeyboardAwareFooter = useCallback(() => {
if (keyboardBehavior === 'hoisted') {
return (
<Keyboard.KeyboardAccessoryView
renderContent={renderFooterContent}
kbInputRef={undefined}
Expand All @@ -263,27 +233,27 @@ const ScreenFooter = (props: ScreenFooterProps) => {
revealKeyboardInteractive
onHeightChanged={setHeight}
/>
</Container>
);
}
);
} else {
return renderFooterContent();
}
}, [keyboardBehavior, renderFooterContent]);

return (
<Animated.View testID={testID} onLayout={onLayout} style={[styles.container, stickyAnimatedStyle, containerStyleOverride]}>
{renderFooterContent()}
<Animated.View
testID={testID}
style={containerStyle}
onLayout={keyboardBehavior === 'hoisted' ? undefined : onLayout}
pointerEvents={!visible ? 'none' : keyboardBehavior === 'hoisted' ? 'box-none' : 'auto'}
>
{renderKeyboardAwareFooter()}
</Animated.View>
);
};

ScreenFooter.displayName = 'ScreenFooter';

const styles = StyleSheet.create({
container: {
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
zIndex: 50
},
contentContainer: {
paddingTop: Spacings.s4,
paddingHorizontal: Spacings.s5,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,19 @@
{
"name": "visible",
"type": "boolean",
"description": "If true, the footer is visible. If false, it slides down",
"description": "If true, the footer is visible. If false, the footer is hidden using the configured animation type",
"default": "true"
},
{
"name": "animationType",
"type": "ScreenFooterAnimationTypeProp",
"description": "The animation type for showing/hiding the footer [slide, fade, none]",
"default": "'slide'"
},
{
"name": "animationDuration",
"type": "number",
"description": "Duration of the show/hide animation in ms",
"description": "Duration of the show/hide animation in ms (sending 0 will disable the animation)",
"default": "200"
},
{
Expand Down
28 changes: 22 additions & 6 deletions packages/react-native-ui-lib/src/components/screenFooter/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,28 @@ export enum ScreenFooterShadow {
SH30 = 'sh30'
}

export interface ScreenFooterProps extends PropsWithChildren<{}> {
export enum ScreenFooterAnimation {
NONE = 'none',
SLIDE = 'slide',
FADE = 'fade'
}

export type ScreenFooterAnimationTypeProp = ScreenFooterAnimation | `${ScreenFooterAnimation}`;

export interface AnimatedFooterStyleProps {
/**
* The type of animation to use when showing or hiding the footer.
* @default 'slide'
*/
animationType?: ScreenFooterAnimationTypeProp;
/**
* Duration of the show/hide animation in ms (sending 0 will disable the animation).
* @default 200
*/
animationDuration?: number;
}

export interface ScreenFooterProps extends AnimatedFooterStyleProps, PropsWithChildren<{}> {
/**
* Used as testing identifier
*/
Expand Down Expand Up @@ -86,11 +107,6 @@ export interface ScreenFooterProps extends PropsWithChildren<{}> {
* If true, the footer is visible. If false, it slides down.
*/
visible?: boolean;
/**
* Duration of the show/hide animation in ms.
* @default 200
*/
animationDuration?: number;
/**
* If true, the footer will respect the safe area (add bottom padding)
*/
Expand Down
Loading
Loading