Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
95eb956
feat: add floating BulkActionBar and use it on the spend page
Krishna2323 Sep 3, 2026
0697580
fix: match BulkActionBar sizing and hover states to the design
Krishna2323 Sep 3, 2026
93344fc
fix: restore the select-all-matching loading state and drop dead code
Krishna2323 Sep 3, 2026
52a13bd
fix: stop the bulk action bar covering the last rows of the list
Krishna2323 Sep 3, 2026
b6ecad7
fix CI failures.
Krishna2323 Sep 3, 2026
0a42dfd
resolve conflicts.
Krishna2323 Sep 3, 2026
64c364d
fix: make the bulk action bar's close button clear the selection
Krishna2323 Sep 3, 2026
0d465e2
feat: polish the bulk action bar from design review
Krishna2323 Sep 7, 2026
1882a95
feat: spring the bulk action bar into place on appear
Krishna2323 Sep 7, 2026
c55dee7
fix text alignment.
Krishna2323 Sep 7, 2026
0bbcdc9
fix: fit the bulk action bar to its container, and tune its entrance
Krishna2323 Sep 7, 2026
af54aa0
fix: keep the bulk action bar's measurement across a fitting reset
Krishna2323 Sep 7, 2026
a455bff
fix: start the bulk action bar at its breakpoint's button count
Krishna2323 Sep 7, 2026
38e7075
fix: stop the bulk action bar flashing wide while it is resized
Krishna2323 Sep 7, 2026
69514ac
Merge branch 'Expensify:main' into krishna2323/issue/98651
Krishna2323 Sep 7, 2026
b5f6f4a
update filter bar padding.
Krishna2323 Sep 8, 2026
6e93cb3
Merge branch 'main' of https://github.com/Expensify/App into krishna2…
Krishna2323 Sep 8, 2026
b31edbe
feat: render the bulk action bar's menus in the app's own theme
Krishna2323 Sep 8, 2026
ed8ab51
fix: follow the comment-style and async-effect standards in the bulk …
Krishna2323 Sep 8, 2026
7831630
fix: scroll long bulk action submenus and reserve the bar's real height
Krishna2323 Sep 8, 2026
ed39042
feat: drop the three dots from the bulk action bar's More button
Krishna2323 Sep 9, 2026
9f7d9dd
fix: address review findings on the bulk action bar
Krishna2323 Sep 9, 2026
26a617f
Merge branch 'Expensify:main' into krishna2323/issue/98651
Krishna2323 Sep 10, 2026
f78f935
fix: surface the export header and all-matching label on the wide bar
Krishna2323 Sep 10, 2026
53964bc
fix: stop the bar from sticking hidden and Esc from breaking RHP
Krishna2323 Sep 10, 2026
d97c7d1
add willAlertModalBecomeVisible condition.
Krishna2323 Sep 10, 2026
941d983
Merge branch 'Expensify:main' into krishna2323/issue/98651
Krishna2323 Sep 10, 2026
d8f6d9a
fix: restore Mobile-Expensify to its clean merge pointer
Krishna2323 Sep 10, 2026
9215f27
merge main.
Krishna2323 Sep 13, 2026
713b192
fix: keep Export under one button and measure the bar against its label
Krishna2323 Sep 13, 2026
217260a
fix tests.
Krishna2323 Sep 13, 2026
b3fb97e
fix: keep the bulk action bar on screen when its measurement key changes
Krishna2323 Sep 15, 2026
1f32572
Merge branch 'Expensify:main' into krishna2323/issue/98651
Krishna2323 Sep 16, 2026
5831bd6
fix: give a bulk action button's submenu the same scroll check as the…
Krishna2323 Sep 16, 2026
edc4128
fix: address review comments.
Krishna2323 Sep 17, 2026
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
34 changes: 33 additions & 1 deletion src/CONST/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6654,6 +6654,35 @@ const CONST = {
DANGER: 'danger',
},

BULK_ACTION_BAR: {
/** How many of a selection's actions get a button of their own in the bar before the rest move behind "More". */
MAX_INLINE_ACTIONS: 3,

/** The same at the in-between widths, which do not have the room for a third button. */
MAX_INLINE_ACTIONS_MEDIUM_SCREEN: 2,

/**
* Room left between the bar and the edges of the container it floats in. The bar sheds an action before it gets
* this close to an edge, rather than only once it has already touched one.
*/
EDGE_MARGIN: 24,

/** How far the bar floats above the bottom of the container it is rendered in. */
BOTTOM_OFFSET: 20,

/** Breathing room left between the bar and the last row of the list it floats over. */
LIST_GAP: 12,

/** How far below its resting place the bar starts before it springs up into view. */
SLIDE_IN_DISTANCE: 24,

/**
* Spring the bar settles into place with. Overdamped, matching the canvas gestures, so it arrives quickly and
* without a bounce. The default spring wobbles noticeably over this short a travel.
*/
SLIDE_IN_SPRING: {mass: 1, stiffness: 1000, damping: 500},
},

BUTTON_REMOVE_BORDER_RADIUS: {
LEFT: 'left',
RIGHT: 'right',
Expand Down Expand Up @@ -8952,6 +8981,10 @@ const CONST = {
BILLING_BANNER: {
RIGHT_ICON: 'BillingBanner-RightIcon',
},
BULK_ACTION_BAR: {
CLEAR_SELECTION: 'BulkActionBar-ClearSelection',
MORE: 'BulkActionBar-More',
},
ACCOUNT_MANAGER_BOOK_CALL: {
BUTTON: 'AccountManagerBookCallButton-Button',
},
Expand Down Expand Up @@ -9084,7 +9117,6 @@ const CONST = {
FILTER_LIMIT: 'Search-FilterLimit',
ADVANCED_FILTERS_BUTTON: 'Search-AdvancedFiltersButton',
COLUMNS_BUTTON: 'Search-ColumnsButton',
BULK_ACTIONS_DROPDOWN: 'Search-BulkActionsDropdown',
SELECT_ALL_CHECKBOX: 'Search-SelectAllCheckbox',
SELECTION_MODE_MENU_ITEM: 'Search-SelectionModeMenuItem',
FILTER_RESET_BUTTON: 'Search-FilterResetButton',
Expand Down
110 changes: 110 additions & 0 deletions src/components/BulkActionBar/BulkActionBarButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import Button from '@components/Button';
import PopoverMenu from '@components/PopoverMenu';

import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
import usePopoverPosition from '@hooks/usePopoverPosition';
import useTheme from '@hooks/useTheme';

import shouldPopoverUseScrollView from '@libs/shouldPopoverUseScrollView';

import CONST from '@src/CONST';
import type {AnchorPosition} from '@src/styles';

import type {View} from 'react-native';

import React, {useEffect, useRef, useState} from 'react';

import type {BulkActionBarButtonProps} from './types';

import BulkActionBarMenuTheme from './BulkActionBarMenuTheme';
import {defaultPopoverAnchorPosition, SUB_MENU_ANCHOR_ALIGNMENT} from './popoverPosition';

/**
* A single action in the bulk action bar. An action that carries `subMenuItems` renders a dropdown caret and opens
* those items in a menu above itself. Every other action runs `onSelected` directly.
*/
function BulkActionBarButton<TValueType>({option, onSubItemSelected}: BulkActionBarButtonProps<TValueType>) {
const theme = useTheme();
const icons = useMemoizedLazyExpensifyIcons(['DownArrow', 'UpArrow']);
const {calculatePopoverPosition} = usePopoverPosition();

const anchorRef = useRef<View | null>(null);
const [isMenuVisible, setIsMenuVisible] = useState(false);
const [anchorPosition, setAnchorPosition] = useState<AnchorPosition | null>(defaultPopoverAnchorPosition);

const subMenuItems = option.subMenuItems;
const hasSubMenu = !!subMenuItems?.length;

useEffect(() => {
if (!anchorRef.current || !isMenuVisible) {
return;
}

// The position is measured asynchronously, so a measurement still in flight when the menu is reopened would
// otherwise land after the newer one and place the menu against the button's previous position.
let ignore = false;

calculatePopoverPosition(anchorRef, SUB_MENU_ANCHOR_ALIGNMENT).then((position) => {
if (ignore) {
return;
}

setAnchorPosition(position);
});

return () => {
ignore = true;
};
}, [isMenuVisible, calculatePopoverPosition]);

return (
<>
<Button
ref={anchorRef}
onPress={() => {
if (hasSubMenu) {
setIsMenuVisible((isVisible) => !isVisible);
return;
}

option.onSelected?.();
}}
size={CONST.BUTTON_SIZE.SMALL}
isDisabled={option.disabled}
accessibilityLabel={option.text}
sentryLabel={option.sentryLabel}
>
{!!option.icon && (
<Button.Icon
src={option.icon}
hoverFill={theme.iconMenuHovered}
/>
)}
<Button.Text>{option.text}</Button.Text>
{hasSubMenu && <Button.Icon src={isMenuVisible ? icons.UpArrow : icons.DownArrow} />}
</Button>
{hasSubMenu && !!anchorPosition && (
<BulkActionBarMenuTheme>
<PopoverMenu
isVisible={isMenuVisible}
anchorRef={anchorRef}
anchorPosition={anchorPosition}
anchorAlignment={SUB_MENU_ANCHOR_ALIGNMENT}
menuItems={subMenuItems.map((subItem) => ({...subItem, shouldCallAfterModalHide: true}))}
onClose={() => setIsMenuVisible(false)}
onItemSelected={(selectedSubItem, index, event) => {
onSubItemSelected?.(selectedSubItem, index, event);
if (selectedSubItem.shouldCloseModalOnSelect === false) {
return;
}
setIsMenuVisible(false);
}}
shouldUseScrollView={shouldPopoverUseScrollView(subMenuItems)}
/>
</BulkActionBarMenuTheme>
)}
</>
);
}

export default BulkActionBarButton;
26 changes: 26 additions & 0 deletions src/components/BulkActionBar/BulkActionBarMenuTheme.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import ThemeProvider from '@components/ThemeProvider';
import ThemeStylesProvider from '@components/ThemeStylesContextProvider';

import useThemePreference from '@hooks/useThemePreference';

import React from 'react';

/**
* Puts the app's own theme back for the menus that open off the bar, which would otherwise inherit the inverted theme
* the bar itself renders under. The bar stays inverted so it stands out against the table. Its menus read as the
* popovers they are everywhere else in the app.
*
* `useThemePreference` reads the preference rather than the surrounding context, so this reports the page's theme even
* from inside the bar's inverted subtree.
*/
function BulkActionBarMenuTheme({children}: React.PropsWithChildren) {
const pageThemePreference = useThemePreference();

return (
<ThemeProvider theme={pageThemePreference}>
<ThemeStylesProvider>{children}</ThemeStylesProvider>
</ThemeProvider>
);
}

export default BulkActionBarMenuTheme;
Loading
Loading