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
2 changes: 1 addition & 1 deletion assets/images/camera.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions assets/images/plus-circle.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
120 changes: 83 additions & 37 deletions src/components/FloatingActionButton.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
import type {ForwardedRef} from 'react';
import React, {useEffect, useRef} from 'react';
// eslint-disable-next-line no-restricted-imports
import type {GestureResponderEvent, Role, Text, View} from 'react-native';
import type {GestureResponderEvent, Role, Text as TextType, View as ViewType} from 'react-native';
import {View} from 'react-native';
import Animated, {Easing, interpolateColor, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated';
import Svg, {Path} from 'react-native-svg';
import useLocalize from '@hooks/useLocalize';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import {canUseTouchScreen} from '@libs/DeviceCapabilities';
import variables from '@styles/variables';
import {PressableWithoutFeedback} from './Pressable';
import CONST from '@src/CONST';
import Icon from './Icon';
import {PlusCircle} from './Icon/Expensicons';
import {PressableWithFeedback, PressableWithoutFeedback} from './Pressable';
import Text from './Text';

const FAB_PATH = 'M12,3c0-1.1-0.9-2-2-2C8.9,1,8,1.9,8,3v5H3c-1.1,0-2,0.9-2,2c0,1.1,0.9,2,2,2h5v5c0,1.1,0.9,2,2,2c1.1,0,2-0.9,2-2v-5h5c1.1,0,2-0.9,2-2c0-1.1-0.9-2-2-2h-5V3z';
const SMALL_FAB_PATH =
Expand All @@ -35,16 +41,17 @@ type FloatingActionButtonProps = {
role: Role;

/** Reference to the outer element */
ref?: ForwardedRef<HTMLDivElement | View | Text>;
ref?: ForwardedRef<HTMLDivElement | ViewType | TextType>;
};

function FloatingActionButton({onPress, onLongPress, isActive, accessibilityLabel, role, ref}: FloatingActionButtonProps) {
const {success, successHover, buttonDefaultBG, textLight} = useTheme();
const {buttonDefaultBG, buttonHoveredBG, icon} = useTheme();
const styles = useThemeStyles();
const borderRadius = styles.floatingActionButton.borderRadius;
const fabPressable = useRef<HTMLDivElement | View | Text | null>(null);
const fabPressable = useRef<HTMLDivElement | ViewType | TextType | null>(null);
const {shouldUseNarrowLayout} = useResponsiveLayout();
const isLHBVisible = !shouldUseNarrowLayout;
const {translate} = useLocalize();

const fabSize = isLHBVisible ? variables.iconSizeSmall : variables.iconSizeNormal;

Expand All @@ -61,7 +68,7 @@ function FloatingActionButton({onPress, onLongPress, isActive, accessibilityLabe
}, [isActive, sharedValue]);

const animatedStyle = useAnimatedStyle(() => {
const backgroundColor = interpolateColor(sharedValue.get(), [0, 1], [success, buttonDefaultBG]);
const backgroundColor = interpolateColor(sharedValue.get(), [0, 1], [buttonDefaultBG, buttonHoveredBG]);

return {
transform: [{rotate: `${sharedValue.get() * 135}deg`}],
Expand All @@ -85,45 +92,84 @@ function FloatingActionButton({onPress, onLongPress, isActive, accessibilityLabe
onLongPress?.(event);
};

if (isLHBVisible) {
return (
<PressableWithoutFeedback
ref={(el) => {
fabPressable.current = el ?? null;
if (buttonRef && 'current' in buttonRef) {
buttonRef.current = el ?? null;
}
}}
style={[
styles.navigationTabBarFABItem,

// Prevent text selection on touch devices (e.g. on long press)
canUseTouchScreen() && styles.userSelectNone,
styles.flex1,
]}
accessibilityLabel={accessibilityLabel}
onPress={toggleFabAction}
onLongPress={longPressFabAction}
role={role}
shouldUseHapticsOnLongPress
testID="floating-action-button"
>
{({hovered}) => (
<Animated.View
style={[styles.floatingActionButton, {borderRadius}, styles.floatingActionButtonSmall, animatedStyle, hovered && {backgroundColor: buttonHoveredBG}]}
testID="fab-animated-container"
>
<Svg
width={fabSize}
height={fabSize}
>
<AnimatedPath
d={isLHBVisible ? SMALL_FAB_PATH : FAB_PATH}
fill={icon}
/>
</Svg>
</Animated.View>
)}
</PressableWithoutFeedback>
);
}

return (
<PressableWithoutFeedback
ref={(el) => {
fabPressable.current = el ?? null;
if (buttonRef && 'current' in buttonRef) {
buttonRef.current = el ?? null;
}
}}
<PressableWithFeedback
onPress={onPress}
role={CONST.ROLE.BUTTON}
accessibilityLabel={translate('common.create')}
wrapperStyle={styles.flex1}
style={[
styles.h100,
styles.navigationTabBarItem,
styles.navigationTabBarFABItem,

// Prevent text selection on touch devices (e.g. on long press)
canUseTouchScreen() && styles.userSelectNone,
styles.flex1,
]}
accessibilityLabel={accessibilityLabel}
onPress={toggleFabAction}
onLongPress={longPressFabAction}
role={role}
shouldUseHapticsOnLongPress
testID="floating-action-button"
testID="create-action-button"
>
{({hovered}) => (
<Animated.View
style={[styles.floatingActionButton, {borderRadius}, isLHBVisible && styles.floatingActionButtonSmall, animatedStyle, hovered && {backgroundColor: successHover}]}
testID="fab-animated-container"
<View
testID="fab-container"
style={styles.navigationTabBarItem}
>
<View>
<Icon
src={PlusCircle}
fill={icon}
width={variables.iconBottomBar}
height={variables.iconBottomBar}
/>
</View>
<Text
numberOfLines={1}
style={[styles.textSmall, styles.textAlignCenter, styles.mt1Half, styles.textSupporting, styles.navigationTabBarLabel]}
>
<Svg
width={fabSize}
height={fabSize}
>
<AnimatedPath
d={isLHBVisible ? SMALL_FAB_PATH : FAB_PATH}
fill={textLight}
/>
</Svg>
</Animated.View>
)}
</PressableWithoutFeedback>
{translate('common.create')}
</Text>
</View>
</PressableWithFeedback>
);
}

Expand Down
91 changes: 91 additions & 0 deletions src/components/FloatingCameraButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import React, {useMemo} from 'react';
import {View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import useLocalize from '@hooks/useLocalize';
import useOnyx from '@hooks/useOnyx';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import {startMoneyRequest} from '@libs/actions/IOU';
import {canUseTouchScreen} from '@libs/DeviceCapabilities';
import interceptAnonymousUser from '@libs/interceptAnonymousUser';
import Navigation from '@libs/Navigation/Navigation';
import {generateReportID, getWorkspaceChats} from '@libs/ReportUtils';
import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils';
import variables from '@styles/variables';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type * as OnyxTypes from '@src/types/onyx';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import Icon from './Icon';
import {Camera} from './Icon/Expensicons';
import {PressableWithoutFeedback} from './Pressable';

const sessionSelector = (session: OnyxEntry<OnyxTypes.Session>) => ({email: session?.email, accountID: session?.accountID});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Does this not exist in the shared selectors? I would think it must exist


function FloatingCameraButton() {
const {textLight} = useTheme();
const styles = useThemeStyles();
const {translate} = useLocalize();

const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID, {canBeMissing: true});
const [activePolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`, {canBeMissing: true});
const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: false, selector: sessionSelector});
const [allTransactionDrafts] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, {canBeMissing: true});
const reportID = useMemo(() => generateReportID(), []);

const [policyChatForActivePolicy] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {
canBeMissing: true,
selector: (reports) => {
if (isEmptyObject(activePolicy) || !activePolicy?.isPolicyExpenseChatEnabled) {
return undefined;
}
const policyChatsForActivePolicy = getWorkspaceChats(activePolicyID, [session?.accountID ?? CONST.DEFAULT_NUMBER_ID], reports);
return policyChatsForActivePolicy.at(0);
},
Comment on lines +39 to +45

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The selectors should not be inline, can you refactor this to use stable reference to selector?

});

const onPress = () => {
interceptAnonymousUser(() => {
if (policyChatForActivePolicy?.policyID && shouldRestrictUserBillableActions(policyChatForActivePolicy.policyID)) {
Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(policyChatForActivePolicy.policyID));
return;
}

const quickActionReportID = policyChatForActivePolicy?.reportID ?? reportID;
startMoneyRequest(CONST.IOU.TYPE.SUBMIT, quickActionReportID, CONST.IOU.REQUEST_TYPE.SCAN, true, undefined, allTransactionDrafts);
});
};

return (
<PressableWithoutFeedback
style={[
styles.navigationTabBarFABItem,
styles.ph0,
// Prevent text selection on touch devices (e.g. on long press)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
// Prevent text selection on touch devices (e.g. on long press)
// Prevent text selection on touch devices (e.g. on long press)

canUseTouchScreen() && styles.userSelectNone,
styles.floatingCameraButton,
]}
accessibilityLabel={translate('sidebarScreen.fabScanReceiptExplained')}
onPress={onPress}
role={CONST.ROLE.BUTTON}
testID="floating-camera-button"
>
<View
style={styles.floatingActionButton}
testID="floating-camera-button-container"
>
<Icon
fill={textLight}
src={Camera}
width={variables.iconSizeNormal}
height={variables.iconSizeNormal}
/>
</View>
</PressableWithoutFeedback>
);
}

FloatingCameraButton.displayName = 'FloatingCameraButton';

export default FloatingCameraButton;
72 changes: 72 additions & 0 deletions src/components/FloatingReceiptButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import React, {useRef} from 'react';
// eslint-disable-next-line no-restricted-imports
Comment thread
JKobrynski marked this conversation as resolved.
import type {GestureResponderEvent, Role, Text, View as ViewType} from 'react-native';
import {View} from 'react-native';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import {canUseTouchScreen} from '@libs/DeviceCapabilities';
import variables from '@styles/variables';
import Icon from './Icon';
import {ReceiptPlus} from './Icon/Expensicons';
import {PressableWithoutFeedback} from './Pressable';

type FloatingReceiptButtonProps = {
/* Callback to fire on request to toggle the FloatingReceiptButton */
onPress: (event: GestureResponderEvent | KeyboardEvent | undefined) => void;

/* An accessibility label for the button */
accessibilityLabel: string;

/* An accessibility role for the button */
role: Role;
};

function FloatingReceiptButton({onPress, accessibilityLabel, role}: FloatingReceiptButtonProps) {
const {successHover, textLight} = useTheme();
const styles = useThemeStyles();
const borderRadius = styles.floatingActionButton.borderRadius;
const fabPressable = useRef<HTMLDivElement | ViewType | Text | null>(null);

const toggleFabAction = (event: GestureResponderEvent | KeyboardEvent | undefined) => {
// Drop focus to avoid blue focus ring.
fabPressable.current?.blur();
onPress(event);
};

return (
<PressableWithoutFeedback
ref={(el) => {
fabPressable.current = el ?? null;
}}
style={[
styles.navigationTabBarFABItem,

// Prevent text selection on touch devices (e.g. on long press)
canUseTouchScreen() && styles.userSelectNone,
]}
accessibilityLabel={accessibilityLabel}
onPress={toggleFabAction}
role={role}
shouldUseHapticsOnLongPress
testID="floating-receipt-button"
>
{({hovered}) => (
<View
style={[styles.floatingActionButton, {borderRadius}, styles.floatingActionButtonSmall, hovered && {backgroundColor: successHover}]}
testID="floating-receipt-button-container"
>
<Icon
fill={textLight}
src={ReceiptPlus}
width={variables.iconSizeSmall}
height={variables.iconSizeSmall}
/>
</View>
)}
</PressableWithoutFeedback>
);
}

FloatingReceiptButton.displayName = 'FloatingReceiptButton';

export default FloatingReceiptButton;
2 changes: 2 additions & 0 deletions src/components/Icon/Expensicons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ import Phone from '@assets/images/phone.svg';
import Pin from '@assets/images/pin.svg';
import Plane from '@assets/images/plane.svg';
import Play from '@assets/images/play.svg';
import PlusCircle from '@assets/images/plus-circle.svg';
import PlusMinus from '@assets/images/plus-minus.svg';
import Plus from '@assets/images/plus.svg';
import Printer from '@assets/images/printer.svg';
Expand Down Expand Up @@ -452,4 +453,5 @@ export {
SageIntacctExport,
XeroExport,
ArrowCircleClockwise,
PlusCircle,
};
2 changes: 1 addition & 1 deletion src/components/Navigation/NavigationTabBar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,7 @@ function NavigationTabBar({selectedTab, isTopLevelBar = false}: NavigationTabBar
onPress={navigateToSettings}
/>
</View>
<View style={styles.leftNavigationTabBarItem}>
<View style={styles.leftNavigationTabBarFAB}>
<NavigationTabBarFloatingActionButton />
</View>
</View>
Expand Down
Loading
Loading