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
14 changes: 9 additions & 5 deletions src/components/AutoCompleteSuggestions/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import React, {useEffect} from 'react';
import type {AutoCompleteSuggestionsProps, MeasureParentContainerAndCursor} from './types';

import AutoCompleteSuggestionsPortal from './AutoCompleteSuggestionsPortal';
import getBottomSuggestionPadding from './AutoCompleteSuggestionsPortal/getBottomSuggestionPadding';
import getLeftOffset from './getSuggestionsLeftOffset';
import getSuggestionsViewportBottom from './getSuggestionsViewportBottom';

Expand Down Expand Up @@ -43,11 +44,13 @@ function isSuggestionMenuRenderedAbove(isEnoughSpaceAboveForBigMenu: boolean, is
}

type IsEnoughSpaceToRenderMenuAboveCursor = Pick<MeasureParentContainerAndCursor, 'y' | 'cursorCoordinates' | 'scrollValue'> & {
contentHeight: number;
menuHeight: number;
topInset: number;
};
function isEnoughSpaceToRenderMenuAboveCursor({y, cursorCoordinates, scrollValue, contentHeight, topInset}: IsEnoughSpaceToRenderMenuAboveCursor): boolean {
return y + (cursorCoordinates.y - scrollValue) > contentHeight + topInset + CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_BOX_MAX_SAFE_DISTANCE;
function isEnoughSpaceToRenderMenuAboveCursor({y, cursorCoordinates, scrollValue, menuHeight, topInset}: IsEnoughSpaceToRenderMenuAboveCursor): boolean {
const gapAboveCursor = Math.max(CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_BOX_MAX_SAFE_DISTANCE, getBottomSuggestionPadding(true));

return y + (cursorCoordinates.y - scrollValue) > menuHeight + gapAboveCursor + topInset;
}

const initialContainerState = {
Expand Down Expand Up @@ -123,7 +126,7 @@ function AutoCompleteSuggestions<TSuggestion>({measureParentContainerAndReportCu
y,
cursorCoordinates,
scrollValue,
contentHeight: contentMaxHeight,
menuHeight: StyleUtils.getAutoCompleteSuggestionContainerHeight(contentMaxHeight),
topInset,
});

Expand All @@ -134,7 +137,7 @@ function AutoCompleteSuggestions<TSuggestion>({measureParentContainerAndReportCu
y,
cursorCoordinates,
scrollValue,
contentHeight: contentMinHeight,
menuHeight: StyleUtils.getAutoCompleteSuggestionContainerHeight(contentMinHeight),
topInset,
});

Expand Down Expand Up @@ -186,6 +189,7 @@ function AutoCompleteSuggestions<TSuggestion>({measureParentContainerAndReportCu
isKeyboardAnimatingRef,
isInLandscapeMode,
insets,
StyleUtils,
]);

// Prevent rendering if container dimensions are not set or if we have no suggestions
Expand Down
6 changes: 2 additions & 4 deletions src/components/DragAndDrop/Provider/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import useDragAndDrop from '@hooks/useDragAndDrop';
import useThemeStyles from '@hooks/useThemeStyles';

import {shouldAcceptDrop} from '@libs/DragAndDropUtils';

import htmlDivElementRef from '@src/types/utils/htmlDivElementRef';
import viewRef from '@src/types/utils/viewRef';

Expand All @@ -13,10 +15,6 @@ import type {DragAndDropActionsContextType, DragAndDropProviderProps, DragAndDro

import {DragAndDropActionsContext, DragAndDropStateContext} from './DragAndDropContext';

function shouldAcceptDrop(event: DragEvent): boolean {
return !!event.dataTransfer?.types.some((type) => type === 'Files');
}

function DragAndDropProvider({children, isDisabled = false, setIsDraggingOver = () => {}}: DragAndDropProviderProps) {
const styles = useThemeStyles();
const dropZone = useRef<HTMLDivElement | View>(null);
Expand Down
4 changes: 3 additions & 1 deletion src/components/DropZone/DropZoneWrapper.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import useDragAndDrop from '@hooks/useDragAndDrop';
import useThemeStyles from '@hooks/useThemeStyles';

import {shouldAcceptDrop} from '@libs/DragAndDropUtils';

import htmlDivElementRef from '@src/types/utils/htmlDivElementRef';
import viewRef from '@src/types/utils/viewRef';

Expand All @@ -21,7 +23,7 @@ function DropZoneWrapper({onDrop, children}: DropZoneWrapperProps) {
const dropZone = useRef<HTMLDivElement | View>(null);

const {isDraggingOver} = useDragAndDrop({
shouldAcceptDrop: (event) => !!event.dataTransfer?.types.some((type) => type === 'Files'),
shouldAcceptDrop,
onDrop,
shouldStopPropagation: false,
shouldHandleDragEvent: false,
Expand Down
73 changes: 73 additions & 0 deletions src/components/ReceiptScanDropZone/ReceiptScanDropTarget.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import DropZoneUI from '@components/DropZone/DropZoneUI';

import useDragAndDrop from '@hooks/useDragAndDrop';
import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
import useLocalize from '@hooks/useLocalize';
import useReceiptScanDrop from '@hooks/useReceiptScanDrop';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';

import {shouldAcceptDrop} from '@libs/DragAndDropUtils';

import htmlDivElementRef from '@src/types/utils/htmlDivElementRef';

import type {StyleProp, View, ViewStyle} from 'react-native';

import React, {useEffect} from 'react';
import {View as RNView} from 'react-native';

type ReceiptScanDropTargetProps = {
/** Ref to the element the drag events are bound to */
targetRef: React.RefObject<View | HTMLDivElement | null>;

dropWrapperStyle?: StyleProp<ViewStyle>;

/** Reports the drag-over state back to the drop zone, which publishes it through DragAndDropStateContext */
onDraggingOverChange: (isDraggingOver: boolean) => void;
};

/**
* Owns the receipt scan drag-and-drop logic and the drop overlay.
*/
function ReceiptScanDropTarget({targetRef, dropWrapperStyle, onDraggingOverChange}: ReceiptScanDropTargetProps) {
const styles = useThemeStyles();
const theme = useTheme();
const {translate} = useLocalize();
const expensifyIcons = useMemoizedLazyExpensifyIcons(['SmartScan']);
const {initScanRequest, auxiliaryUI, isDragDisabled} = useReceiptScanDrop();

const {isDraggingOver} = useDragAndDrop({
dropZone: htmlDivElementRef(targetRef),
onDrop: initScanRequest,
shouldAcceptDrop,
isDisabled: isDragDisabled,
});

useEffect(() => {
onDraggingOverChange(isDraggingOver);
return () => onDraggingOverChange(false);
}, [isDraggingOver, onDraggingOverChange]);

return (
<>
{isDraggingOver && (
<RNView
pointerEvents="none"
style={[styles.fullScreen, styles.pAbsolute, styles.invisibleOverlay]}
>
<DropZoneUI
icon={expensifyIcons.SmartScan}
dropTitle={translate('dropzone.scanReceipts')}
dropStyles={styles.receiptDropOverlay(true)}
dropTextStyles={styles.receiptDropText}
dropWrapperStyles={dropWrapperStyle}
dashedBorderStyles={[styles.dropzoneArea, styles.easeInOpacityTransition, styles.activeDropzoneDashedBorder(theme.receiptDropBorderColorActive, true)]}
/>
</RNView>
)}
{auxiliaryUI}
</>
);
}

export default ReceiptScanDropTarget;
77 changes: 30 additions & 47 deletions src/components/ReceiptScanDropZone/index.tsx
Original file line number Diff line number Diff line change
@@ -1,61 +1,44 @@
import DropZoneUI from '@components/DropZone/DropZoneUI';

import useDragAndDrop from '@hooks/useDragAndDrop';
import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
import useLocalize from '@hooks/useLocalize';
import useReceiptScanDrop from '@hooks/useReceiptScanDrop';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';

import htmlDivElementRef from '@src/types/utils/htmlDivElementRef';
import {DragAndDropStateContext} from '@components/DragAndDrop/Provider/DragAndDropContext';

import type {ReactNode, RefObject} from 'react';
import type {StyleProp, View, ViewStyle} from 'react-native';

import React from 'react';
import {View as RNView} from 'react-native';
import React, {useState} from 'react';

import ReceiptScanDropTarget from './ReceiptScanDropTarget';

type ReceiptScanDropZoneProps = {
targetRef: React.RefObject<View | HTMLDivElement | null>;
dropWrapperStyle?: StyleProp<ViewStyle>;
};
/** The page content that receipts can be dropped onto */
children: ReactNode;

function shouldAcceptDrop(event: DragEvent): boolean {
return !!event.dataTransfer?.types.some((type) => type === 'Files');
}
/** Ref to the container receipts are dropped onto */
dropZoneRef: RefObject<View | HTMLDivElement | null>;

/** Whether the drop zone is disabled, keeping the scan logic unmounted */
isDisabled?: boolean;

function ReceiptScanDropZone({targetRef, dropWrapperStyle}: ReceiptScanDropZoneProps) {
const styles = useThemeStyles();
const theme = useTheme();
const {translate} = useLocalize();
const expensifyIcons = useMemoizedLazyExpensifyIcons(['SmartScan']);
const {initScanRequest, auxiliaryUI, isDragDisabled} = useReceiptScanDrop();
dropWrapperStyle?: StyleProp<ViewStyle>;
};

const {isDraggingOver} = useDragAndDrop({
dropZone: htmlDivElementRef(targetRef),
onDrop: initScanRequest,
shouldAcceptDrop,
isDisabled: isDragDisabled,
});
/**
* Turns the page into a receipt scan drop zone and publishes the drag state to it, so components inside can react to a
* file being dragged over the page.
*/
function ReceiptScanDropZone({children, dropZoneRef, isDisabled = false, dropWrapperStyle}: ReceiptScanDropZoneProps) {
const [isDraggingOver, setIsDraggingOver] = useState(false);

return (
<>
{isDraggingOver && (
<RNView
pointerEvents="none"
style={[styles.fullScreen, styles.pAbsolute, styles.invisibleOverlay]}
>
<DropZoneUI
icon={expensifyIcons.SmartScan}
dropTitle={translate('dropzone.scanReceipts')}
dropStyles={styles.receiptDropOverlay(true)}
dropTextStyles={styles.receiptDropText}
dropWrapperStyles={dropWrapperStyle}
dashedBorderStyles={[styles.dropzoneArea, styles.easeInOpacityTransition, styles.activeDropzoneDashedBorder(theme.receiptDropBorderColorActive, true)]}
/>
</RNView>
// eslint-disable-next-line react/jsx-no-constructed-context-values
<DragAndDropStateContext.Provider value={{isDraggingOver, dropZoneID: ''}}>
{children}
{!isDisabled && (
<ReceiptScanDropTarget
targetRef={dropZoneRef}
dropWrapperStyle={dropWrapperStyle}
onDraggingOverChange={setIsDraggingOver}
/>
)}
{auxiliaryUI}
</>
</DragAndDropStateContext.Provider>
);
}

Expand Down
9 changes: 9 additions & 0 deletions src/libs/DragAndDropUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/**
* Whether the dragged content contains files, which is the only payload our drop zones handle.
*/
function shouldAcceptDrop(event: DragEvent): boolean {
return !!event.dataTransfer?.types.some((type) => type === 'Files');
}

// eslint-disable-next-line import/prefer-default-export
export {shouldAcceptDrop};
Loading
Loading