Skip to content

Commit 22c3805

Browse files
M-i-k-e-lCopilot
andauthored
ScreenFooter - add animationType (#3994)
* ScreenFooter - revert "local" withoutAnimation * Refactor keyboardBehavior * Prettify * Refactor animation into useAnimatedFooterStyle Co-authored-by: Copilot <copilot@github.com> * Unify to a single useAnimatedStyle * Rename to animatedValue * Add animationType Co-authored-by: Copilot <copilot@github.com> * Remove keyboardBehavior from deps * Add animationType to FloatingButton * Fix visible's doc * Fix fade\none receiving taps when not visible (Android) * Fix footer no re-appearing on scroll up (Android + remove animation + slide) --------- Co-authored-by: Copilot <copilot@github.com>
1 parent 0b214d6 commit 22c3805

7 files changed

Lines changed: 187 additions & 90 deletions

File tree

demo/src/screens/componentScreens/ScreenFooterScreen.tsx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
ScreenFooterLayouts,
1111
ScreenFooterBackgrounds,
1212
KeyboardBehavior,
13+
ScreenFooterAnimationTypeProp,
1314
FooterAlignment,
1415
HorizontalItemsDistribution,
1516
ItemsFit,
@@ -88,6 +89,12 @@ const KEYBOARD_BEHAVIOR_OPTIONS = [
8889
{label: 'Hoisted', value: KeyboardBehavior.HOISTED}
8990
];
9091

92+
const ANIMATION_TYPE_OPTIONS = [
93+
{label: 'Slide', value: 'slide'},
94+
{label: 'Fade', value: 'fade'},
95+
{label: 'None', value: 'none'}
96+
];
97+
9198
const KEYBOARD_BEHAVIOR_OPTIONS_SPACED = [
9299
{label: 'Sticky', value: KeyboardBehavior.STICKY},
93100
{label: 'Hoisted', value: KeyboardBehavior.HOISTED},
@@ -115,6 +122,7 @@ const ScreenFooterContent = () => {
115122
const [layout, setLayout] = useState<ScreenFooterLayouts>(ScreenFooterLayouts.HORIZONTAL);
116123
const [background, setBackground] = useState<ScreenFooterBackgrounds>(ScreenFooterBackgrounds.SOLID);
117124
const [keyboardBehavior, setKeyboardBehavior] = useState<KeyboardBehavior>(KeyboardBehavior.STICKY);
125+
const [animationType, setAnimationType] = useState<ScreenFooterAnimationTypeProp>('slide');
118126
const [alignment, setAlignment] = useState<FooterAlignment>(FooterAlignment.CENTER);
119127
const [horizontalAlignment, setHorizontalAlignment] = useState<FooterAlignment>(FooterAlignment.CENTER);
120128
const [distribution, setDistribution] = useState<HorizontalItemsDistribution>(HorizontalItemsDistribution.STACK);
@@ -390,6 +398,21 @@ const ScreenFooterContent = () => {
390398
</View>
391399
</View>
392400

401+
{/* Animation type */}
402+
<View marginB-s4>
403+
<Text text70M marginB-s2>
404+
Animation Type
405+
</Text>
406+
<View row>
407+
<SegmentedControl
408+
segments={ANIMATION_TYPE_OPTIONS}
409+
initialIndex={ANIMATION_TYPE_OPTIONS.findIndex(opt => opt.value === animationType)}
410+
onChangeIndex={index => setAnimationType(ANIMATION_TYPE_OPTIONS[index].value)}
411+
/>
412+
<View flex />
413+
</View>
414+
</View>
415+
393416
{/* Alignment (Cross Axis) */}
394417
<View marginB-s4>
395418
<Text text70M marginB-s2>
@@ -478,6 +501,7 @@ const ScreenFooterContent = () => {
478501
layout={layout}
479502
backgroundType={background}
480503
keyboardBehavior={keyboardBehavior}
504+
animationType={animationType}
481505
alignment={alignment}
482506
horizontalAlignment={horizontalAlignment}
483507
horizontalItemsDistribution={distribution}

packages/react-native-ui-lib/src/components/floatingButton/index.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export enum FloatingButtonLayouts {
1111
HORIZONTAL = 'Horizontal'
1212
}
1313

14-
export interface FloatingButtonProps extends Pick<ScreenFooterProps, 'isAndroidEdgeToEdge'> {
14+
export interface FloatingButtonProps extends Pick<ScreenFooterProps, 'isAndroidEdgeToEdge' | 'animationType'> {
1515
/**
1616
* Whether the button is visible
1717
*/
@@ -82,6 +82,7 @@ const FloatingButton = (props: FloatingButtonProps) => {
8282
hideBackgroundOverlay,
8383
hoisted = Constants.isAndroid,
8484
isAndroidEdgeToEdge,
85+
animationType,
8586
testID
8687
} = props;
8788

@@ -162,6 +163,7 @@ const FloatingButton = (props: FloatingButtonProps) => {
162163
keyboardBehavior={hoisted ? KeyboardBehavior.HOISTED : KeyboardBehavior.STICKY}
163164
isAndroidEdgeToEdge={isAndroidEdgeToEdge}
164165
animationDuration={withoutAnimation ? 0 : duration}
166+
animationType={animationType}
165167
itemsFit={fullWidth ? ItemsFit.STRETCH : undefined}
166168
contentContainerStyle={footerContentContainerStyle}
167169
testID={testID}

packages/react-native-ui-lib/src/components/screenFooter/index.tsx

Lines changed: 51 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import React, {useCallback, useEffect, useMemo, useState} from 'react';
1+
import React, {useCallback, useMemo} from 'react';
22
import {LayoutChangeEvent, StyleSheet, ViewStyle} from 'react-native';
3-
import Animated, {useAnimatedKeyboard, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated';
3+
import Animated from 'react-native-reanimated';
44
import {Keyboard} from 'uilib-native';
55
import {SafeAreaContextPackage} from '../../optionalDependencies';
66
import View from '../view';
@@ -9,6 +9,7 @@ import Assets from '../../assets';
99
import {Colors, Shadows, Spacings} from '../../style';
1010
import {asBaseComponent, Constants} from '../../commons/new';
1111
import {useKeyboardHeight} from '../../hooks';
12+
import useAnimatedFooterStyle from './useAnimatedFooterStyle';
1213
import {
1314
ScreenFooterProps,
1415
ScreenFooterLayouts,
@@ -17,6 +18,7 @@ import {
1718
HorizontalItemsDistribution,
1819
ItemsFit,
1920
KeyboardBehavior,
21+
ScreenFooterAnimationTypeProp,
2022
ScreenFooterShadow
2123
} from './types';
2224

@@ -28,9 +30,9 @@ export {
2830
HorizontalItemsDistribution,
2931
ItemsFit,
3032
KeyboardBehavior,
33+
ScreenFooterAnimationTypeProp,
3134
ScreenFooterShadow
3235
};
33-
const androidVersion = Constants.getAndroidVersion();
3436
const ScreenFooter = (props: ScreenFooterProps) => {
3537
const {
3638
testID,
@@ -44,41 +46,22 @@ const ScreenFooter = (props: ScreenFooterProps) => {
4446
itemWidth,
4547
horizontalItemsDistribution: distribution,
4648
visible = true,
47-
animationDuration = 200,
49+
animationDuration,
50+
animationType,
4851
shadow = ScreenFooterShadow.SH20,
4952
hideDivider = false,
50-
isAndroidEdgeToEdge = !!androidVersion && androidVersion >= 35 ? true : undefined,
53+
isAndroidEdgeToEdge,
5154
containerStyle: containerStyleOverride,
5255
contentContainerStyle: contentContainerStyleOverride
5356
} = props;
5457

55-
const withoutAnimation = animationDuration === 0;
56-
57-
const keyboard = useAnimatedKeyboard({
58-
isNavigationBarTranslucentAndroid: isAndroidEdgeToEdge,
59-
isStatusBarTranslucentAndroid: isAndroidEdgeToEdge
60-
});
61-
const [height, setHeight] = useState(0);
62-
const visibilityTranslateY = useSharedValue(0);
63-
64-
// Update visibility translation when visible or height changes
65-
useEffect(() => {
66-
visibilityTranslateY.value = withTiming(visible ? 0 : height, {duration: animationDuration});
67-
}, [visible, height, animationDuration, visibilityTranslateY]);
68-
69-
// Animated style for STICKY behavior (counters Android system offset + visibility)
70-
const stickyAnimatedStyle = useAnimatedStyle(() => {
71-
const counterSystemOffset = Constants.isAndroid ? keyboard.height.value : 0;
72-
return {
73-
transform: [{translateY: counterSystemOffset + visibilityTranslateY.value}]
74-
};
75-
});
76-
77-
// Animated style for HOISTED behavior (visibility only, keyboard handled by KeyboardAccessoryView)
78-
const hoistedAnimatedStyle = useAnimatedStyle(() => {
79-
return {
80-
transform: [{translateY: visibilityTranslateY.value}]
81-
};
58+
const {containerStyle, setHeight} = useAnimatedFooterStyle({
59+
animationDuration,
60+
animationType,
61+
keyboardBehavior,
62+
visible,
63+
isAndroidEdgeToEdge,
64+
containerStyle: containerStyleOverride
8265
});
8366

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

205-
const renderChild = useCallback(
206-
(child: React.ReactNode, index: number) => {
207-
if (itemsFit === ItemsFit.FIXED && itemWidth) {
208-
const fixedStyle: ViewStyle = isHorizontal
209-
? {width: itemWidth, flexShrink: 1, overflow: 'hidden', flexDirection: 'row', justifyContent: 'center'}
210-
: {width: itemWidth, maxWidth: '100%'};
211-
return (
212-
<View key={index} style={fixedStyle}>
213-
{child}
214-
</View>
215-
);
216-
}
188+
const renderChild = useCallback((child: React.ReactNode, index: number) => {
189+
if (itemsFit === ItemsFit.FIXED && itemWidth) {
190+
const fixedStyle: ViewStyle = isHorizontal
191+
? {width: itemWidth, flexShrink: 1, overflow: 'hidden', flexDirection: 'row', justifyContent: 'center'}
192+
: {width: itemWidth, maxWidth: '100%'};
193+
return (
194+
<View key={index} style={fixedStyle}>
195+
{child}
196+
</View>
197+
);
198+
}
217199

218-
if (isHorizontal && React.isValidElement(child) && itemsFit === ItemsFit.STRETCH) {
219-
return (
220-
<View flex row centerH key={index}>
221-
{child}
222-
</View>
223-
);
224-
}
225-
return child;
226-
},
227-
[itemsFit, itemWidth, isHorizontal]
228-
);
200+
if (isHorizontal && React.isValidElement(child) && itemsFit === ItemsFit.STRETCH) {
201+
return (
202+
<View flex row centerH key={index}>
203+
{child}
204+
</View>
205+
);
206+
}
207+
return child;
208+
},
209+
[itemsFit, itemWidth, isHorizontal]);
229210

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

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

243-
const Container = useMemo(() => {
244-
return withoutAnimation ? View : Animated.View;
245-
}, [withoutAnimation]);
246-
247-
const containerStyle = useMemo(() => {
248-
return withoutAnimation
249-
? [styles.container, containerStyleOverride]
250-
: [styles.container, hoistedAnimatedStyle, containerStyleOverride];
251-
// eslint-disable-next-line react-hooks/exhaustive-deps
252-
}, [withoutAnimation, containerStyleOverride]);
253-
254-
if (keyboardBehavior === KeyboardBehavior.HOISTED) {
255-
return (
256-
<Container style={containerStyle} pointerEvents={visible ? 'box-none' : 'none'}>
224+
const renderKeyboardAwareFooter = useCallback(() => {
225+
if (keyboardBehavior === 'hoisted') {
226+
return (
257227
<Keyboard.KeyboardAccessoryView
258228
renderContent={renderFooterContent}
259229
kbInputRef={undefined}
@@ -263,27 +233,27 @@ const ScreenFooter = (props: ScreenFooterProps) => {
263233
revealKeyboardInteractive
264234
onHeightChanged={setHeight}
265235
/>
266-
</Container>
267-
);
268-
}
236+
);
237+
} else {
238+
return renderFooterContent();
239+
}
240+
}, [keyboardBehavior, renderFooterContent]);
269241

270242
return (
271-
<Animated.View testID={testID} onLayout={onLayout} style={[styles.container, stickyAnimatedStyle, containerStyleOverride]}>
272-
{renderFooterContent()}
243+
<Animated.View
244+
testID={testID}
245+
style={containerStyle}
246+
onLayout={keyboardBehavior === 'hoisted' ? undefined : onLayout}
247+
pointerEvents={!visible ? 'none' : keyboardBehavior === 'hoisted' ? 'box-none' : 'auto'}
248+
>
249+
{renderKeyboardAwareFooter()}
273250
</Animated.View>
274251
);
275252
};
276253

277254
ScreenFooter.displayName = 'ScreenFooter';
278255

279256
const styles = StyleSheet.create({
280-
container: {
281-
position: 'absolute',
282-
bottom: 0,
283-
left: 0,
284-
right: 0,
285-
zIndex: 50
286-
},
287257
contentContainer: {
288258
paddingTop: Spacings.s4,
289259
paddingHorizontal: Spacings.s5,

packages/react-native-ui-lib/src/components/screenFooter/screenFooter.api.json

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,13 +55,19 @@
5555
{
5656
"name": "visible",
5757
"type": "boolean",
58-
"description": "If true, the footer is visible. If false, it slides down",
58+
"description": "If true, the footer is visible. If false, the footer is hidden using the configured animation type",
5959
"default": "true"
6060
},
61+
{
62+
"name": "animationType",
63+
"type": "ScreenFooterAnimationTypeProp",
64+
"description": "The animation type for showing/hiding the footer [slide, fade, none]",
65+
"default": "'slide'"
66+
},
6167
{
6268
"name": "animationDuration",
6369
"type": "number",
64-
"description": "Duration of the show/hide animation in ms",
70+
"description": "Duration of the show/hide animation in ms (sending 0 will disable the animation)",
6571
"default": "200"
6672
},
6773
{

packages/react-native-ui-lib/src/components/screenFooter/types.ts

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,28 @@ export enum ScreenFooterShadow {
4040
SH30 = 'sh30'
4141
}
4242

43-
export interface ScreenFooterProps extends PropsWithChildren<{}> {
43+
export enum ScreenFooterAnimation {
44+
NONE = 'none',
45+
SLIDE = 'slide',
46+
FADE = 'fade'
47+
}
48+
49+
export type ScreenFooterAnimationTypeProp = ScreenFooterAnimation | `${ScreenFooterAnimation}`;
50+
51+
export interface AnimatedFooterStyleProps {
52+
/**
53+
* The type of animation to use when showing or hiding the footer.
54+
* @default 'slide'
55+
*/
56+
animationType?: ScreenFooterAnimationTypeProp;
57+
/**
58+
* Duration of the show/hide animation in ms (sending 0 will disable the animation).
59+
* @default 200
60+
*/
61+
animationDuration?: number;
62+
}
63+
64+
export interface ScreenFooterProps extends AnimatedFooterStyleProps, PropsWithChildren<{}> {
4465
/**
4566
* Used as testing identifier
4667
*/
@@ -86,11 +107,6 @@ export interface ScreenFooterProps extends PropsWithChildren<{}> {
86107
* If true, the footer is visible. If false, it slides down.
87108
*/
88109
visible?: boolean;
89-
/**
90-
* Duration of the show/hide animation in ms.
91-
* @default 200
92-
*/
93-
animationDuration?: number;
94110
/**
95111
* If true, the footer will respect the safe area (add bottom padding)
96112
*/

0 commit comments

Comments
 (0)