From f73f6c7f0d3c16c1cbb105c889dc46e9add21ecd Mon Sep 17 00:00:00 2001
From: Matiss Janis Aboltins <matiss@mja.lv>
Date: Tue, 7 May 2024 17:18:51 +0100
Subject: [PATCH] :recycle: (tooltip) refactoring to react-aria (vol.3) (#2631)

---
 .../desktop-client/e2e/transactions.test.js   |   2 +-
 .../components/autocomplete/Autocomplete.tsx  | 228 +++++-----
 .../autocomplete/CategoryAutocomplete.tsx     |  14 +-
 .../src/components/common/Input.tsx           |   4 +-
 .../src/components/common/Popover.tsx         |  30 +-
 .../src/components/common/Select.tsx          |   2 +-
 .../components/filters/FilterExpression.tsx   |  17 +-
 .../src/components/filters/FilterMenu.tsx     |  63 ++-
 .../src/components/filters/FiltersMenu.jsx    | 429 ++++++++++--------
 .../src/components/filters/NameFilter.tsx     |   7 +-
 .../filters/SavedFilterMenuButton.tsx         |  27 +-
 .../src/components/select/DateSelect.tsx      |  61 +--
 .../transactions/TransactionsTable.test.jsx   |  71 +--
 upcoming-release-notes/2631.md                |   6 +
 14 files changed, 523 insertions(+), 438 deletions(-)
 create mode 100644 upcoming-release-notes/2631.md

diff --git a/packages/desktop-client/e2e/transactions.test.js b/packages/desktop-client/e2e/transactions.test.js
index 2d2f40dbf..f0ea08d79 100644
--- a/packages/desktop-client/e2e/transactions.test.js
+++ b/packages/desktop-client/e2e/transactions.test.js
@@ -46,7 +46,7 @@ test.describe('Transactions', () => {
       await expect(datepicker).toMatchThemeScreenshots();
 
       // Select "is xxxxx"
-      await datepicker.getByRole('button', { name: '20' }).click();
+      await datepicker.getByText('20', { exact: true }).click();
       await filterTooltip.applyButton.click();
 
       // Assert that there are no transactions
diff --git a/packages/desktop-client/src/components/autocomplete/Autocomplete.tsx b/packages/desktop-client/src/components/autocomplete/Autocomplete.tsx
index 2f0b15b34..dfd66b245 100644
--- a/packages/desktop-client/src/components/autocomplete/Autocomplete.tsx
+++ b/packages/desktop-client/src/components/autocomplete/Autocomplete.tsx
@@ -16,11 +16,11 @@ import { css } from 'glamor';
 
 import { SvgRemove } from '../../icons/v2';
 import { useResponsive } from '../../ResponsiveProvider';
-import { theme, type CSSProperties, styles } from '../../style';
+import { theme, styles } from '../../style';
 import { Button } from '../common/Button';
 import { Input } from '../common/Input';
+import { Popover } from '../common/Popover';
 import { View } from '../common/View';
-import { Tooltip } from '../tooltips';
 
 type CommonAutocompleteProps<T extends Item> = {
   focused?: boolean;
@@ -31,8 +31,6 @@ type CommonAutocompleteProps<T extends Item> = {
     onChange?: (value: string) => void;
   };
   suggestions?: T[];
-  tooltipStyle?: CSSProperties;
-  tooltipProps?: ComponentProps<typeof Tooltip>;
   renderInput?: (props: ComponentProps<typeof Input>) => ReactNode;
   renderItems?: (
     items: T[],
@@ -214,8 +212,6 @@ function SingleAutocomplete<T extends Item>({
   labelProps = {},
   inputProps = {},
   suggestions,
-  tooltipStyle,
-  tooltipProps,
   renderInput = defaultRenderInput,
   renderItems = defaultRenderItems,
   itemToString = defaultItemToString,
@@ -253,6 +249,8 @@ function SingleAutocomplete<T extends Item>({
     onClose?.();
   };
 
+  const triggerRef = useRef(null);
+
   const { isNarrowWidth } = useResponsive();
   const narrowInputStyle = isNarrowWidth
     ? {
@@ -442,115 +440,122 @@ function SingleAutocomplete<T extends Item>({
         // can't use a View here, but we can fake it be using the
         // className
         <div className={`view ${css({ display: 'flex' })}`} {...containerProps}>
-          {renderInput(
-            getInputProps({
-              focused,
-              ...inputProps,
-              onFocus: e => {
-                inputProps.onFocus?.(e);
-
-                if (openOnFocus) {
-                  open();
-                }
-              },
-              onBlur: e => {
-                // Should this be e.nativeEvent
-                e['preventDownshiftDefault'] = true;
-                inputProps.onBlur?.(e);
-
-                if (!closeOnBlur) return;
-
-                if (clearOnBlur) {
-                  if (e.target.value === '') {
-                    onSelect?.(null, e.target.value);
-                    setSelectedItem(null);
-                    close();
-                    return;
+          <View ref={triggerRef} style={{ flexShrink: 0 }}>
+            {renderInput(
+              getInputProps({
+                focused,
+                ...inputProps,
+                onFocus: e => {
+                  inputProps.onFocus?.(e);
+
+                  if (openOnFocus) {
+                    open();
                   }
+                },
+                onBlur: e => {
+                  // Should this be e.nativeEvent
+                  e['preventDownshiftDefault'] = true;
+                  inputProps.onBlur?.(e);
+
+                  if (!closeOnBlur) return;
+
+                  if (clearOnBlur) {
+                    if (e.target.value === '') {
+                      onSelect?.(null, e.target.value);
+                      setSelectedItem(null);
+                      close();
+                      return;
+                    }
 
-                  // If not using table behavior, reset the input on blur. Tables
-                  // handle saving the value on blur.
-                  const value = selectedItem ? getItemId(selectedItem) : null;
-
-                  resetState(value);
-                } else {
-                  close();
-                }
-              },
-              onKeyDown: (e: KeyboardEvent<HTMLInputElement>) => {
-                const { onKeyDown } = inputProps || {};
-
-                // If the dropdown is open, an item is highlighted, and the user
-                // pressed enter, always capture that and handle it ourselves
-                if (isOpen) {
-                  if (e.key === 'Enter') {
-                    if (highlightedIndex != null) {
-                      if (
-                        inst.lastChangeType ===
-                        Downshift.stateChangeTypes.itemMouseEnter
-                      ) {
-                        // If the last thing the user did was hover an item, intentionally
-                        // ignore the default behavior of selecting the item. It's too
-                        // common to accidentally hover an item and then save it
-                        e.preventDefault();
-                      } else {
-                        // Otherwise, stop propagation so that the table navigator
-                        // doesn't handle it
+                    // If not using table behavior, reset the input on blur. Tables
+                    // handle saving the value on blur.
+                    const value = selectedItem ? getItemId(selectedItem) : null;
+
+                    resetState(value);
+                  } else {
+                    close();
+                  }
+                },
+                onKeyDown: (e: KeyboardEvent<HTMLInputElement>) => {
+                  const { onKeyDown } = inputProps || {};
+
+                  // If the dropdown is open, an item is highlighted, and the user
+                  // pressed enter, always capture that and handle it ourselves
+                  if (isOpen) {
+                    if (e.key === 'Enter') {
+                      if (highlightedIndex != null) {
+                        if (
+                          inst.lastChangeType ===
+                          Downshift.stateChangeTypes.itemMouseEnter
+                        ) {
+                          // If the last thing the user did was hover an item, intentionally
+                          // ignore the default behavior of selecting the item. It's too
+                          // common to accidentally hover an item and then save it
+                          e.preventDefault();
+                        } else {
+                          // Otherwise, stop propagation so that the table navigator
+                          // doesn't handle it
+                          e.stopPropagation();
+                        }
+                      } else if (!strict) {
+                        // Handle it ourselves
                         e.stopPropagation();
+                        onSelect(value, (e.target as HTMLInputElement).value);
+                        return onSelectAfter();
+                      } else {
+                        // No highlighted item, still allow the table to save the item
+                        // as `null`, even though we're allowing the table to move
+                        e.preventDefault();
+                        onKeyDown?.(e);
                       }
-                    } else if (!strict) {
-                      // Handle it ourselves
-                      e.stopPropagation();
-                      onSelect(value, (e.target as HTMLInputElement).value);
-                      return onSelectAfter();
-                    } else {
-                      // No highlighted item, still allow the table to save the item
-                      // as `null`, even though we're allowing the table to move
+                    } else if (shouldSaveFromKey(e)) {
                       e.preventDefault();
                       onKeyDown?.(e);
                     }
-                  } else if (shouldSaveFromKey(e)) {
-                    e.preventDefault();
-                    onKeyDown?.(e);
                   }
-                }
 
-                // Handle escape ourselves
-                if (e.key === 'Escape') {
-                  e.nativeEvent['preventDownshiftDefault'] = true;
+                  // Handle escape ourselves
+                  if (e.key === 'Escape') {
+                    e.nativeEvent['preventDownshiftDefault'] = true;
 
-                  if (!embedded) {
-                    e.stopPropagation();
-                  }
+                    if (!embedded) {
+                      e.stopPropagation();
+                    }
 
-                  fireUpdate(
-                    onUpdate,
-                    strict,
-                    suggestions,
-                    null,
-                    getItemId(originalItem),
-                  );
-
-                  setValue(getItemName(originalItem));
-                  setSelectedItem(findItem(strict, suggestions, originalItem));
-                  setHighlightedIndex(null);
-                  if (embedded) {
-                    open();
-                  } else {
-                    close();
+                    fireUpdate(
+                      onUpdate,
+                      strict,
+                      suggestions,
+                      null,
+                      getItemId(originalItem),
+                    );
+
+                    setValue(getItemName(originalItem));
+                    setSelectedItem(
+                      findItem(strict, suggestions, originalItem),
+                    );
+                    setHighlightedIndex(null);
+                    if (embedded) {
+                      open();
+                    } else {
+                      close();
+                    }
                   }
-                }
-              },
-              onChange: (e: ChangeEvent<HTMLInputElement>) => {
-                const { onChange } = inputProps || {};
-                onChange?.(e.target.value);
-              },
-            }),
-          )}
+                },
+                onChange: (e: ChangeEvent<HTMLInputElement>) => {
+                  const { onChange } = inputProps || {};
+                  onChange?.(e.target.value);
+                },
+              }),
+            )}
+          </View>
           {isOpen &&
             filtered.length > 0 &&
             (embedded ? (
-              <View style={{ marginTop: 5 }} data-testid="autocomplete">
+              <View
+                style={{ ...styles.darkScrollbar, marginTop: 5 }}
+                data-testid="autocomplete"
+              >
                 {renderItems(
                   filtered,
                   getItemProps,
@@ -559,17 +564,20 @@ function SingleAutocomplete<T extends Item>({
                 )}
               </View>
             ) : (
-              <Tooltip
-                position="bottom-stretch"
+              <Popover
+                triggerRef={triggerRef}
+                placement="bottom start"
                 offset={2}
+                isOpen={isOpen}
+                onOpenChange={close}
+                isNonModal
                 style={{
-                  padding: 0,
+                  ...styles.darkScrollbar,
                   backgroundColor: theme.menuAutoCompleteBackground,
                   color: theme.menuAutoCompleteText,
                   minWidth: 200,
-                  ...tooltipStyle,
+                  width: triggerRef.current?.clientWidth,
                 }}
-                {...tooltipProps}
                 data-testid="autocomplete"
               >
                 {renderItems(
@@ -578,7 +586,7 @@ function SingleAutocomplete<T extends Item>({
                   highlightedIndex,
                   inputValue,
                 )}
-              </Tooltip>
+              </Popover>
             ))}
         </div>
       )}
@@ -626,13 +634,8 @@ function MultiAutocomplete<T extends Item>({
   ...props
 }: MultiAutocompleteProps<T>) {
   const [focused, setFocused] = useState(false);
-  const lastSelectedItems = useRef<typeof selectedItems>();
   const selectedItemIds = selectedItems.map(getItemId);
 
-  useEffect(() => {
-    lastSelectedItems.current = selectedItems;
-  });
-
   function onRemoveItem(id: T['id']) {
     const items = selectedItemIds.filter(i => i !== id);
     onSelect(items);
@@ -669,9 +672,6 @@ function MultiAutocomplete<T extends Item>({
       onSelect={onAddItem}
       highlightFirst
       strict={strict}
-      tooltipProps={{
-        forceLayout: lastSelectedItems.current !== selectedItems,
-      }}
       renderInput={inputProps => (
         <View
           style={{
diff --git a/packages/desktop-client/src/components/autocomplete/CategoryAutocomplete.tsx b/packages/desktop-client/src/components/autocomplete/CategoryAutocomplete.tsx
index a742bcbfc..b9b418a54 100644
--- a/packages/desktop-client/src/components/autocomplete/CategoryAutocomplete.tsx
+++ b/packages/desktop-client/src/components/autocomplete/CategoryAutocomplete.tsx
@@ -70,6 +70,14 @@ function CategoryList({
 }: CategoryListProps) {
   let lastGroup: string | undefined | null = null;
 
+  const filteredItems = useMemo(
+    () =>
+      showHiddenItems
+        ? items
+        : items.filter(item => !item.hidden && !item.group?.hidden),
+    [showHiddenItems, items],
+  );
+
   return (
     <View>
       <View
@@ -79,7 +87,7 @@ function CategoryList({
           ...(!embedded && { maxHeight: 175 }),
         }}
       >
-        {items.map((item, idx) => {
+        {filteredItems.map((item, idx) => {
           if (item.id === 'split') {
             return renderSplitTransactionButton({
               key: 'split',
@@ -89,10 +97,6 @@ function CategoryList({
             });
           }
 
-          if ((item.hidden || item.group?.hidden) && !showHiddenItems) {
-            return <Fragment key={item.id} />;
-          }
-
           const showGroup = item.cat_group !== lastGroup;
           const groupName = `${item.group?.name}${item.group?.hidden ? ' (hidden)' : ''}`;
           lastGroup = item.cat_group;
diff --git a/packages/desktop-client/src/components/common/Input.tsx b/packages/desktop-client/src/components/common/Input.tsx
index 576d933ba..317c23a76 100644
--- a/packages/desktop-client/src/components/common/Input.tsx
+++ b/packages/desktop-client/src/components/common/Input.tsx
@@ -66,6 +66,8 @@ export function Input({
       )}`}
       {...nativeProps}
       onKeyDown={e => {
+        nativeProps.onKeyDown?.(e);
+
         if (e.key === 'Enter' && onEnter) {
           onEnter(e);
         }
@@ -73,8 +75,6 @@ export function Input({
         if (e.key === 'Escape' && onEscape) {
           onEscape(e);
         }
-
-        nativeProps.onKeyDown?.(e);
       }}
       onBlur={e => {
         onUpdate?.(e.target.value);
diff --git a/packages/desktop-client/src/components/common/Popover.tsx b/packages/desktop-client/src/components/common/Popover.tsx
index 94b029f74..834c24e1c 100644
--- a/packages/desktop-client/src/components/common/Popover.tsx
+++ b/packages/desktop-client/src/components/common/Popover.tsx
@@ -1,16 +1,42 @@
 import { type ComponentProps } from 'react';
 import { Popover as ReactAriaPopover } from 'react-aria-components';
 
+import { css } from 'glamor';
+
 import { styles } from '../../style';
 
 type PopoverProps = ComponentProps<typeof ReactAriaPopover>;
 
-export const Popover = ({ style = {}, ...props }: PopoverProps) => {
+export const Popover = ({
+  style = {},
+  shouldCloseOnInteractOutside,
+  ...props
+}: PopoverProps) => {
   return (
     <ReactAriaPopover
       placement="bottom end"
       offset={0}
-      style={{ ...styles.tooltip, padding: 0, ...style }}
+      className={`${css({
+        ...styles.tooltip,
+        ...styles.lightScrollbar,
+        padding: 0,
+        ...style,
+      })}`}
+      shouldCloseOnInteractOutside={element => {
+        // Disable closing the popover when a reach listbox is clicked (Select component)
+        if (
+          element.getAttribute('data-reach-listbox-list') !== null ||
+          element.getAttribute('data-reach-listbox-option') !== null
+        ) {
+          return false;
+        }
+
+        if (shouldCloseOnInteractOutside) {
+          return shouldCloseOnInteractOutside(element);
+        }
+
+        return true;
+      }}
       {...props}
     />
   );
diff --git a/packages/desktop-client/src/components/common/Select.tsx b/packages/desktop-client/src/components/common/Select.tsx
index f5fd9c26b..06c26d712 100644
--- a/packages/desktop-client/src/components/common/Select.tsx
+++ b/packages/desktop-client/src/components/common/Select.tsx
@@ -98,7 +98,7 @@ export function Select<Value extends string>({
       </ListboxButton>
       <ListboxPopover
         style={{
-          zIndex: 10000,
+          zIndex: 100000,
           outline: 0,
           borderRadius: styles.menuBorderRadius,
           backgroundColor: theme.menuBackground,
diff --git a/packages/desktop-client/src/components/filters/FilterExpression.tsx b/packages/desktop-client/src/components/filters/FilterExpression.tsx
index 5a258e951..e72a657cb 100644
--- a/packages/desktop-client/src/components/filters/FilterExpression.tsx
+++ b/packages/desktop-client/src/components/filters/FilterExpression.tsx
@@ -1,4 +1,4 @@
-import React, { useState } from 'react';
+import React, { useRef, useState } from 'react';
 
 import { mapField, friendlyOp } from 'loot-core/src/shared/rules';
 import { integerToCurrency } from 'loot-core/src/shared/util';
@@ -10,6 +10,7 @@ import {
 import { SvgDelete } from '../../icons/v0';
 import { type CSSProperties, theme } from '../../style';
 import { Button } from '../common/Button';
+import { Popover } from '../common/Popover';
 import { Text } from '../common/Text';
 import { View } from '../common/View';
 import { Value } from '../rules/Value';
@@ -39,6 +40,7 @@ export function FilterExpression({
   onDelete,
 }: FilterExpressionProps) {
   const [editing, setEditing] = useState(false);
+  const triggerRef = useRef(null);
 
   const field = subfieldFromFilter({ field: originalField, value });
 
@@ -55,6 +57,7 @@ export function FilterExpression({
       }}
     >
       <Button
+        ref={triggerRef}
         type="bare"
         disabled={customName != null}
         onClick={() => setEditing(true)}
@@ -89,7 +92,15 @@ export function FilterExpression({
           }}
         />
       </Button>
-      {editing && (
+
+      <Popover
+        triggerRef={triggerRef}
+        placement="bottom start"
+        isOpen={editing}
+        onOpenChange={() => setEditing(false)}
+        style={{ width: 275, padding: 15, color: theme.menuItemText }}
+        data-testid="filters-menu-tooltip"
+      >
         <FilterEditor
           field={originalField}
           op={op}
@@ -102,7 +113,7 @@ export function FilterExpression({
           onSave={onChange}
           onClose={() => setEditing(false)}
         />
-      )}
+      </Popover>
     </View>
   );
 }
diff --git a/packages/desktop-client/src/components/filters/FilterMenu.tsx b/packages/desktop-client/src/components/filters/FilterMenu.tsx
index a392fd177..b56649567 100644
--- a/packages/desktop-client/src/components/filters/FilterMenu.tsx
+++ b/packages/desktop-client/src/components/filters/FilterMenu.tsx
@@ -1,54 +1,49 @@
 import React from 'react';
 
 import { Menu } from '../common/Menu';
-import { MenuTooltip } from '../common/MenuTooltip';
 
 import { type SavedFilter } from './SavedFilterMenuButton';
 
 export function FilterMenu({
-  onClose,
   filterId,
   onFilterMenuSelect,
 }: {
-  onClose: () => void;
   filterId: SavedFilter;
   onFilterMenuSelect: (item: string) => void;
 }) {
   return (
-    <MenuTooltip width={200} onClose={onClose}>
-      <Menu
-        onMenuSelect={item => {
-          onFilterMenuSelect(item);
-        }}
-        items={
-          !filterId.id
+    <Menu
+      onMenuSelect={item => {
+        onFilterMenuSelect(item);
+      }}
+      items={
+        !filterId.id
+          ? [
+              { name: 'save-filter', text: 'Save new filter' },
+              { name: 'clear-filter', text: 'Clear all conditions' },
+            ]
+          : filterId.id !== null && filterId.status === 'saved'
             ? [
+                { name: 'rename-filter', text: 'Rename' },
+                { name: 'delete-filter', text: 'Delete' },
+                Menu.line,
+                {
+                  name: 'save-filter',
+                  text: 'Save new filter',
+                  disabled: true,
+                },
+                { name: 'clear-filter', text: 'Clear all conditions' },
+              ]
+            : [
+                { name: 'rename-filter', text: 'Rename' },
+                { name: 'update-filter', text: 'Update condtions' },
+                { name: 'reload-filter', text: 'Revert changes' },
+                { name: 'delete-filter', text: 'Delete' },
+                Menu.line,
                 { name: 'save-filter', text: 'Save new filter' },
                 { name: 'clear-filter', text: 'Clear all conditions' },
               ]
-            : filterId.id !== null && filterId.status === 'saved'
-              ? [
-                  { name: 'rename-filter', text: 'Rename' },
-                  { name: 'delete-filter', text: 'Delete' },
-                  Menu.line,
-                  {
-                    name: 'save-filter',
-                    text: 'Save new filter',
-                    disabled: true,
-                  },
-                  { name: 'clear-filter', text: 'Clear all conditions' },
-                ]
-              : [
-                  { name: 'rename-filter', text: 'Rename' },
-                  { name: 'update-filter', text: 'Update condtions' },
-                  { name: 'reload-filter', text: 'Revert changes' },
-                  { name: 'delete-filter', text: 'Delete' },
-                  Menu.line,
-                  { name: 'save-filter', text: 'Save new filter' },
-                  { name: 'clear-filter', text: 'Clear all conditions' },
-                ]
-        }
-      />
-    </MenuTooltip>
+      }
+    />
   );
 }
diff --git a/packages/desktop-client/src/components/filters/FiltersMenu.jsx b/packages/desktop-client/src/components/filters/FiltersMenu.jsx
index db8459deb..1897bd296 100644
--- a/packages/desktop-client/src/components/filters/FiltersMenu.jsx
+++ b/packages/desktop-client/src/components/filters/FiltersMenu.jsx
@@ -25,6 +25,7 @@ import { theme } from '../../style';
 import { Button } from '../common/Button';
 import { HoverTarget } from '../common/HoverTarget';
 import { Menu } from '../common/Menu';
+import { Popover } from '../common/Popover';
 import { Select } from '../common/Select';
 import { Stack } from '../common/Stack';
 import { Text } from '../common/Text';
@@ -39,6 +40,8 @@ import { subfieldFromFilter } from './subfieldFromFilter';
 import { subfieldToOptions } from './subfieldToOptions';
 import { updateFilterReducer } from './updateFilterReducer';
 
+let isDatepickerClick = false;
+
 const filterFields = [
   'date',
   'account',
@@ -80,171 +83,166 @@ function ConfigureField({
   }
 
   return (
-    <Tooltip
-      position="bottom-left"
-      style={{ padding: 15, color: theme.menuItemText }}
-      width={275}
-      onClose={() => dispatch({ type: 'close' })}
-      data-testid="filters-menu-tooltip"
-    >
-      <FocusScope>
-        <View style={{ marginBottom: 10 }}>
-          <Stack direction="row" align="flex-start">
-            {field === 'amount' || field === 'date' ? (
-              <Select
-                bare
-                options={
-                  field === 'amount'
+    <FocusScope>
+      <View style={{ marginBottom: 10 }}>
+        <Stack direction="row" align="flex-start">
+          {field === 'amount' || field === 'date' ? (
+            <Select
+              bare
+              options={
+                field === 'amount'
+                  ? [
+                      ['amount', 'Amount'],
+                      ['amount-inflow', 'Amount (inflow)'],
+                      ['amount-outflow', 'Amount (outflow)'],
+                    ]
+                  : field === 'date'
                     ? [
-                        ['amount', 'Amount'],
-                        ['amount-inflow', 'Amount (inflow)'],
-                        ['amount-outflow', 'Amount (outflow)'],
+                        ['date', 'Date'],
+                        ['month', 'Month'],
+                        ['year', 'Year'],
                       ]
-                    : field === 'date'
-                      ? [
-                          ['date', 'Date'],
-                          ['month', 'Month'],
-                          ['year', 'Year'],
-                        ]
-                      : null
-                }
-                value={subfield}
-                onChange={sub => {
-                  setSubfield(sub);
-
-                  if (sub === 'month' || sub === 'year') {
-                    dispatch({ type: 'set-op', op: 'is' });
-                  }
-                }}
-                style={{ borderWidth: 1 }}
-              />
-            ) : (
-              titleFirst(mapField(field))
-            )}
-            <View style={{ flex: 1 }} />
-          </Stack>
-        </View>
-
-        <View
-          style={{
-            color: theme.pageTextLight,
-            marginBottom: 10,
-          }}
-        >
-          {field === 'saved' && 'Existing filters will be cleared'}
-        </View>
+                    : null
+              }
+              value={subfield}
+              onChange={sub => {
+                setSubfield(sub);
 
-        <Stack
-          direction="row"
-          align="flex-start"
-          spacing={1}
-          style={{ flexWrap: 'wrap' }}
-        >
-          {type === 'boolean' ? (
-            <>
-              <OpButton
-                key="true"
-                op="true"
-                selected={value === true}
-                onClick={() => {
-                  dispatch({ type: 'set-op', op: 'is' });
-                  dispatch({ type: 'set-value', value: true });
-                }}
-              />
-              <OpButton
-                key="false"
-                op="false"
-                selected={value === false}
-                onClick={() => {
+                if (sub === 'month' || sub === 'year') {
                   dispatch({ type: 'set-op', op: 'is' });
-                  dispatch({ type: 'set-value', value: false });
-                }}
-              />
-            </>
+                }
+              }}
+              style={{ borderWidth: 1 }}
+            />
           ) : (
-            <>
-              <Stack
-                direction="row"
-                align="flex-start"
-                spacing={1}
-                style={{ flexWrap: 'wrap' }}
-              >
-                {ops.slice(0, 3).map(currOp => (
-                  <OpButton
-                    key={currOp}
-                    op={currOp}
-                    selected={currOp === op}
-                    onClick={() => dispatch({ type: 'set-op', op: currOp })}
-                  />
-                ))}
-              </Stack>
-              <Stack
-                direction="row"
-                align="flex-start"
-                spacing={1}
-                style={{ flexWrap: 'wrap' }}
-              >
-                {ops.slice(3, ops.length).map(currOp => (
-                  <OpButton
-                    key={currOp}
-                    op={currOp}
-                    selected={currOp === op}
-                    onClick={() => dispatch({ type: 'set-op', op: currOp })}
-                  />
-                ))}
-              </Stack>
-            </>
+            titleFirst(mapField(field))
           )}
+          <View style={{ flex: 1 }} />
         </Stack>
+      </View>
 
-        <form action="#">
-          {type !== 'boolean' && (
-            <GenericInput
-              inputRef={inputRef}
-              field={field}
-              subfield={subfield}
-              type={
-                type === 'id' && (op === 'contains' || op === 'doesNotContain')
-                  ? 'string'
-                  : type
-              }
-              value={value}
-              multi={op === 'oneOf' || op === 'notOneOf'}
-              style={{ marginTop: 10 }}
-              onChange={v => dispatch({ type: 'set-value', value: v })}
-            />
-          )}
+      <View
+        style={{
+          color: theme.pageTextLight,
+          marginBottom: 10,
+        }}
+      >
+        {field === 'saved' && 'Existing filters will be cleared'}
+      </View>
 
-          <Stack
-            direction="row"
-            justify="flex-end"
-            align="center"
-            style={{ marginTop: 15 }}
-          >
-            <View style={{ flex: 1 }} />
-            <Button
-              type="primary"
-              onClick={e => {
-                e.preventDefault();
-                onApply({
-                  field,
-                  op,
-                  value,
-                  options: subfieldToOptions(field, subfield),
-                });
+      <Stack
+        direction="row"
+        align="flex-start"
+        spacing={1}
+        style={{ flexWrap: 'wrap' }}
+      >
+        {type === 'boolean' ? (
+          <>
+            <OpButton
+              key="true"
+              op="true"
+              selected={value === true}
+              onClick={() => {
+                dispatch({ type: 'set-op', op: 'is' });
+                dispatch({ type: 'set-value', value: true });
+              }}
+            />
+            <OpButton
+              key="false"
+              op="false"
+              selected={value === false}
+              onClick={() => {
+                dispatch({ type: 'set-op', op: 'is' });
+                dispatch({ type: 'set-value', value: false });
               }}
+            />
+          </>
+        ) : (
+          <>
+            <Stack
+              direction="row"
+              align="flex-start"
+              spacing={1}
+              style={{ flexWrap: 'wrap' }}
+            >
+              {ops.slice(0, 3).map(currOp => (
+                <OpButton
+                  key={currOp}
+                  op={currOp}
+                  selected={currOp === op}
+                  onClick={() => dispatch({ type: 'set-op', op: currOp })}
+                />
+              ))}
+            </Stack>
+            <Stack
+              direction="row"
+              align="flex-start"
+              spacing={1}
+              style={{ flexWrap: 'wrap' }}
             >
-              Apply
-            </Button>
-          </Stack>
-        </form>
-      </FocusScope>
-    </Tooltip>
+              {ops.slice(3, ops.length).map(currOp => (
+                <OpButton
+                  key={currOp}
+                  op={currOp}
+                  selected={currOp === op}
+                  onClick={() => dispatch({ type: 'set-op', op: currOp })}
+                />
+              ))}
+            </Stack>
+          </>
+        )}
+      </Stack>
+
+      <form action="#">
+        {type !== 'boolean' && (
+          <GenericInput
+            inputRef={inputRef}
+            field={field}
+            subfield={subfield}
+            type={
+              type === 'id' && (op === 'contains' || op === 'doesNotContain')
+                ? 'string'
+                : type
+            }
+            value={value}
+            multi={op === 'oneOf' || op === 'notOneOf'}
+            style={{ marginTop: 10 }}
+            onChange={v => {
+              dispatch({ type: 'set-value', value: v });
+            }}
+          />
+        )}
+
+        <Stack
+          direction="row"
+          justify="flex-end"
+          align="center"
+          style={{ marginTop: 15 }}
+        >
+          <View style={{ flex: 1 }} />
+          <Button
+            type="primary"
+            onClick={e => {
+              e.preventDefault();
+              onApply({
+                field,
+                op,
+                value,
+                options: subfieldToOptions(field, subfield),
+              });
+            }}
+          >
+            Apply
+          </Button>
+        </Stack>
+      </form>
+    </FocusScope>
   );
 }
 
 export function FilterButton({ onApply, compact, hover, exclude }) {
   const filters = useFilters();
+  const triggerRef = useRef(null);
 
   const dateFormat = useDateFormat() || 'MM/dd/yyyy';
 
@@ -322,61 +320,90 @@ export function FilterButton({ onApply, compact, hover, exclude }) {
 
   return (
     <View>
-      <HoverTarget
-        style={{ flexShrink: 0 }}
-        renderContent={() =>
-          hover && (
-            <Tooltip
-              position="bottom-left"
-              style={{
-                lineHeight: 1.5,
-                padding: '6px 10px',
-                backgroundColor: theme.menuBackground,
-                color: theme.menuItemText,
-              }}
-            >
-              <Text>Filters</Text>
-            </Tooltip>
-          )
-        }
+      <View ref={triggerRef}>
+        <HoverTarget
+          style={{ flexShrink: 0 }}
+          renderContent={() =>
+            hover && (
+              <Tooltip
+                position="bottom-left"
+                style={{
+                  lineHeight: 1.5,
+                  padding: '6px 10px',
+                  backgroundColor: theme.menuBackground,
+                  color: theme.menuItemText,
+                }}
+              >
+                <Text>Filters</Text>
+              </Tooltip>
+            )
+          }
+        >
+          {compact ? (
+            <CompactFiltersButton
+              onClick={() => dispatch({ type: 'select-field' })}
+            />
+          ) : (
+            <FiltersButton onClick={() => dispatch({ type: 'select-field' })} />
+          )}
+        </HoverTarget>
+      </View>
+
+      <Popover
+        triggerRef={triggerRef}
+        placement="bottom start"
+        isOpen={state.fieldsOpen}
+        onOpenChange={() => dispatch({ type: 'close' })}
+        data-testid="filters-select-tooltip"
       >
-        {compact ? (
-          <CompactFiltersButton
-            onClick={() => dispatch({ type: 'select-field' })}
+        <Menu
+          onMenuSelect={name => {
+            dispatch({ type: 'configure', field: name });
+          }}
+          items={filterFields
+            .filter(f => (exclude ? !exclude.includes(f[0]) : true))
+            .map(([name, text]) => ({
+              name,
+              text: titleFirst(text),
+            }))}
+        />
+      </Popover>
+
+      <Popover
+        triggerRef={triggerRef}
+        placement="bottom start"
+        isOpen={state.condOpen}
+        onOpenChange={() => {
+          dispatch({ type: 'close' });
+        }}
+        shouldCloseOnInteractOutside={element => {
+          // Datepicker selections for some reason register 2x clicks
+          // We want to keep the popover open after selecting a date.
+          // So we ignore the "close" event on selection + the subsequent event.
+          if (element.dataset.pikaYear) {
+            isDatepickerClick = true;
+            return false;
+          }
+          if (isDatepickerClick) {
+            isDatepickerClick = false;
+            return false;
+          }
+
+          return true;
+        }}
+        style={{ width: 275, padding: 15, color: theme.menuItemText }}
+        data-testid="filters-menu-tooltip"
+      >
+        {state.field && (
+          <ConfigureField
+            field={state.field}
+            op={state.op}
+            value={state.value}
+            dispatch={dispatch}
+            onApply={onValidateAndApply}
           />
-        ) : (
-          <FiltersButton onClick={() => dispatch({ type: 'select-field' })} />
         )}
-      </HoverTarget>
-      {state.fieldsOpen && (
-        <Tooltip
-          position="bottom-left"
-          style={{ padding: 0 }}
-          onClose={() => dispatch({ type: 'close' })}
-          data-testid="filters-select-tooltip"
-        >
-          <Menu
-            onMenuSelect={name => {
-              dispatch({ type: 'configure', field: name });
-            }}
-            items={filterFields
-              .filter(f => (exclude ? !exclude.includes(f[0]) : true))
-              .map(([name, text]) => ({
-                name,
-                text: titleFirst(text),
-              }))}
-          />
-        </Tooltip>
-      )}
-      {state.condOpen && (
-        <ConfigureField
-          field={state.field}
-          op={state.op}
-          value={state.value}
-          dispatch={dispatch}
-          onApply={onValidateAndApply}
-        />
-      )}
+      </Popover>
     </View>
   );
 }
diff --git a/packages/desktop-client/src/components/filters/NameFilter.tsx b/packages/desktop-client/src/components/filters/NameFilter.tsx
index 3165921f9..0e4285e85 100644
--- a/packages/desktop-client/src/components/filters/NameFilter.tsx
+++ b/packages/desktop-client/src/components/filters/NameFilter.tsx
@@ -3,13 +3,11 @@ import React, { useRef, useEffect } from 'react';
 import { theme } from '../../style';
 import { Button } from '../common/Button';
 import { Input } from '../common/Input';
-import { MenuTooltip } from '../common/MenuTooltip';
 import { Stack } from '../common/Stack';
 import { Text } from '../common/Text';
 import { FormField, FormLabel } from '../forms';
 
 export function NameFilter({
-  onClose,
   menuItem,
   name,
   setName,
@@ -17,7 +15,6 @@ export function NameFilter({
   onAddUpdate,
   err,
 }: {
-  onClose: () => void;
   menuItem: string;
   name: string;
   setName: (item: string) => void;
@@ -34,7 +31,7 @@ export function NameFilter({
   }, []);
 
   return (
-    <MenuTooltip width={325} onClose={onClose}>
+    <>
       {menuItem !== 'update-filter' && (
         <form>
           <Stack
@@ -74,6 +71,6 @@ export function NameFilter({
           <Text style={{ color: theme.errorText }}>{err}</Text>
         </Stack>
       )}
-    </MenuTooltip>
+    </>
   );
 }
diff --git a/packages/desktop-client/src/components/filters/SavedFilterMenuButton.tsx b/packages/desktop-client/src/components/filters/SavedFilterMenuButton.tsx
index dc1aa9dc6..10e1176fa 100644
--- a/packages/desktop-client/src/components/filters/SavedFilterMenuButton.tsx
+++ b/packages/desktop-client/src/components/filters/SavedFilterMenuButton.tsx
@@ -1,10 +1,11 @@
-import React, { useState } from 'react';
+import React, { useRef, useState } from 'react';
 
 import { send, sendCatch } from 'loot-core/src/platform/client/fetch';
 import { type RuleConditionEntity } from 'loot-core/types/models/rule';
 
 import { SvgExpandArrow } from '../../icons/v0';
 import { Button } from '../common/Button';
+import { Popover } from '../common/Popover';
 import { Text } from '../common/Text';
 import { View } from '../common/View';
 
@@ -37,6 +38,7 @@ export function SavedFilterMenuButton({
   const [nameOpen, setNameOpen] = useState(false);
   const [adding, setAdding] = useState(false);
   const [menuOpen, setMenuOpen] = useState(false);
+  const triggerRef = useRef(null);
   const [err, setErr] = useState(null);
   const [menuItem, setMenuItem] = useState('');
   const [name, setName] = useState(filterId.name);
@@ -157,6 +159,7 @@ export function SavedFilterMenuButton({
     <View>
       {filters.length > 0 && (
         <Button
+          ref={triggerRef}
           type="bare"
           style={{ marginTop: 10 }}
           onClick={() => {
@@ -180,16 +183,26 @@ export function SavedFilterMenuButton({
           <SvgExpandArrow width={8} height={8} style={{ marginRight: 5 }} />
         </Button>
       )}
-      {menuOpen && (
+
+      <Popover
+        triggerRef={triggerRef}
+        isOpen={menuOpen}
+        onOpenChange={() => setMenuOpen(false)}
+        style={{ width: 200 }}
+      >
         <FilterMenu
-          onClose={() => setMenuOpen(false)}
           filterId={filterId}
           onFilterMenuSelect={onFilterMenuSelect}
         />
-      )}
-      {nameOpen && (
+      </Popover>
+
+      <Popover
+        triggerRef={triggerRef}
+        isOpen={nameOpen}
+        onOpenChange={() => setNameOpen(false)}
+        style={{ width: 325 }}
+      >
         <NameFilter
-          onClose={() => setNameOpen(false)}
           menuItem={menuItem}
           name={name}
           setName={setName}
@@ -197,7 +210,7 @@ export function SavedFilterMenuButton({
           onAddUpdate={onAddUpdate}
           err={err}
         />
-      )}
+      </Popover>
     </View>
   );
 }
diff --git a/packages/desktop-client/src/components/select/DateSelect.tsx b/packages/desktop-client/src/components/select/DateSelect.tsx
index ba3e5431b..badd799fa 100644
--- a/packages/desktop-client/src/components/select/DateSelect.tsx
+++ b/packages/desktop-client/src/components/select/DateSelect.tsx
@@ -27,10 +27,10 @@ import {
 import { stringToInteger } from 'loot-core/src/shared/util';
 
 import { useLocalPref } from '../../hooks/useLocalPref';
-import { type CSSProperties, theme } from '../../style';
+import { theme } from '../../style';
 import { Input } from '../common/Input';
+import { Popover } from '../common/Popover';
 import { View } from '../common/View';
-import { Tooltip } from '../tooltips';
 
 import DateSelectLeft from './DateSelect.left.png';
 import DateSelectRight from './DateSelect.right.png';
@@ -174,7 +174,6 @@ function defaultShouldSaveFromKey(e) {
 type DateSelectProps = {
   containerProps?: ComponentProps<typeof View>;
   inputProps?: ComponentProps<typeof Input>;
-  tooltipStyle?: CSSProperties;
   value: string;
   isOpen?: boolean;
   embedded?: boolean;
@@ -191,7 +190,6 @@ type DateSelectProps = {
 export function DateSelect({
   containerProps,
   inputProps,
-  tooltipStyle,
   value: defaultValue,
   isOpen,
   embedded,
@@ -323,17 +321,23 @@ export function DateSelect({
   }
 
   const maybeWrapTooltip = content => {
-    return embedded ? (
-      content
-    ) : (
-      <Tooltip
-        position="bottom-left"
+    if (embedded) {
+      return open ? content : null;
+    }
+
+    return (
+      <Popover
+        triggerRef={inputRef}
+        placement="bottom start"
         offset={2}
-        style={{ padding: 0, minWidth: 225, ...tooltipStyle }}
+        isOpen={open}
+        isNonModal
+        onOpenChange={() => setOpen(false)}
+        style={{ minWidth: 225 }}
         data-testid="date-select-tooltip"
       >
         {content}
-      </Tooltip>
+      </Popover>
     );
   };
 
@@ -381,24 +385,23 @@ export function DateSelect({
           }
         }}
       />
-      {open &&
-        maybeWrapTooltip(
-          <DatePicker
-            ref={picker}
-            value={selectedValue}
-            firstDayOfWeekIdx={firstDayOfWeekIdx}
-            dateFormat={dateFormat}
-            onUpdate={date => {
-              setSelectedValue(format(date, dateFormat));
-              onUpdate?.(format(date, 'yyyy-MM-dd'));
-            }}
-            onSelect={date => {
-              setValue(format(date, dateFormat));
-              onSelect(format(date, 'yyyy-MM-dd'));
-              setOpen(false);
-            }}
-          />,
-        )}
+      {maybeWrapTooltip(
+        <DatePicker
+          ref={picker}
+          value={selectedValue}
+          firstDayOfWeekIdx={firstDayOfWeekIdx}
+          dateFormat={dateFormat}
+          onUpdate={date => {
+            setSelectedValue(format(date, dateFormat));
+            onUpdate?.(format(date, 'yyyy-MM-dd'));
+          }}
+          onSelect={date => {
+            setValue(format(date, dateFormat));
+            onSelect(format(date, 'yyyy-MM-dd'));
+            setOpen(false);
+          }}
+        />,
+      )}
     </View>
   );
 }
diff --git a/packages/desktop-client/src/components/transactions/TransactionsTable.test.jsx b/packages/desktop-client/src/components/transactions/TransactionsTable.test.jsx
index 075813883..8df4a8c76 100644
--- a/packages/desktop-client/src/components/transactions/TransactionsTable.test.jsx
+++ b/packages/desktop-client/src/components/transactions/TransactionsTable.test.jsx
@@ -440,10 +440,12 @@ describe('Transactions', () => {
 
     const categories = categoryGroups.flatMap(group => group.categories);
     const input = await editField(container, 'category', 2);
-    const tooltip = container.querySelector('[data-testid="autocomplete"]');
-    expect(tooltip).toBeTruthy();
     expect(
-      [...tooltip.querySelectorAll('[data-testid*="category-item"]')].length,
+      [
+        ...screen
+          .getByTestId('autocomplete')
+          .querySelectorAll('[data-testid*="category-item"]'),
+      ].length,
     ).toBe(categoryGroups.length + categories.length);
 
     await userEvent.clear(input);
@@ -451,7 +453,9 @@ describe('Transactions', () => {
 
     // Make sure the list is filtered, the right items exist, and the
     // first item is highlighted
-    let items = tooltip.querySelectorAll('[data-testid*="category-item"]');
+    let items = screen
+      .getByTestId('autocomplete')
+      .querySelectorAll('[data-testid*="category-item"]');
     expect(items.length).toBe(2);
     expect(items[0].textContent).toBe('Usual Expenses');
     expect(items[1].textContent).toBe('General 129.87');
@@ -461,7 +465,9 @@ describe('Transactions', () => {
     await userEvent.clear(input);
     await userEvent.type(input, 'Usual Expenses');
 
-    items = tooltip.querySelectorAll('[data-testid$="category-item"]');
+    items = screen
+      .getByTestId('autocomplete')
+      .querySelectorAll('[data-testid$="category-item"]');
     expect(items.length).toBe(0);
   });
 
@@ -469,16 +475,19 @@ describe('Transactions', () => {
     const { container, getTransactions } = renderTransactions();
 
     const input = await editField(container, 'category', 2);
-    const tooltip = container.querySelector('[data-testid="autocomplete"]');
 
     // No item should be highlighted
-    let highlighted = tooltip.querySelector('[data-highlighted]');
+    let highlighted = screen
+      .getByTestId('autocomplete')
+      .querySelector('[data-highlighted]');
     expect(highlighted).toBeNull();
 
     await userEvent.keyboard('[ArrowDown][ArrowDown][ArrowDown][ArrowDown]');
 
     // The right item should be highlighted
-    highlighted = tooltip.querySelector('[data-highlighted]');
+    highlighted = screen
+      .getByTestId('autocomplete')
+      .querySelector('[data-highlighted]');
     expect(highlighted).not.toBeNull();
     expect(highlighted.textContent).toBe('General 129.87');
 
@@ -509,18 +518,22 @@ describe('Transactions', () => {
 
     await editField(container, 'category', 2);
 
-    let tooltip = container.querySelector('[data-testid="autocomplete"]');
-
     // Make sure none of the items are highlighted
-    const items = tooltip.querySelectorAll('[data-testid$="category-item"]');
-    let highlighted = tooltip.querySelector('[data-highlighted]');
+    const items = screen
+      .getByTestId('autocomplete')
+      .querySelectorAll('[data-testid$="category-item"]');
+    let highlighted = screen
+      .getByTestId('autocomplete')
+      .querySelector('[data-highlighted]');
     expect(highlighted).toBeNull();
 
     // Hover over an item
     await userEvent.hover(items[2]);
 
     // Make sure the expected category is highlighted
-    highlighted = tooltip.querySelector('[data-highlighted]');
+    highlighted = screen
+      .getByTestId('autocomplete')
+      .querySelector('[data-highlighted]');
     expect(highlighted).not.toBeNull();
     expect(highlighted.textContent).toBe('General 129.87');
 
@@ -535,8 +548,7 @@ describe('Transactions', () => {
     );
 
     // It should still be editing the category
-    tooltip = container.querySelector('[data-testid="autocomplete"]');
-    expect(tooltip).toBe(null);
+    expect(screen.queryByTestId('autocomplete')).toBe(null);
     expectToBeEditingField(container, 'category', 2);
   });
 
@@ -545,16 +557,19 @@ describe('Transactions', () => {
 
     const input = await editField(container, 'category', 2);
     const oldCategory = getTransactions()[2].category;
-    const tooltip = container.querySelector('[data-testid="autocomplete"]');
 
-    const items = tooltip.querySelectorAll('[data-testid$="category-item"]');
+    const items = screen
+      .getByTestId('autocomplete')
+      .querySelectorAll('[data-testid$="category-item"]');
 
     // Hover over a few of the items to highlight them
     await userEvent.hover(items[2]);
     await userEvent.hover(items[3]);
 
     // Make sure one of them is highlighted
-    const highlighted = tooltip.querySelectorAll('[data-highlighted]');
+    const highlighted = screen
+      .getByTestId('autocomplete')
+      .querySelectorAll('[data-highlighted]');
     expect(highlighted).toHaveLength(1);
 
     // Navigate away from the field with the keyboard
@@ -618,8 +633,7 @@ describe('Transactions', () => {
     expect(input.value).toBe(oldValue);
 
     // The tooltip be closed
-    const tooltip = container.querySelector('[data-testid="autocomplete"]');
-    expect(tooltip).toBeNull();
+    expect(screen.queryByTestId('autocomplete')).toBeNull();
   });
 
   test('adding a new transaction works', async () => {
@@ -667,10 +681,7 @@ describe('Transactions', () => {
     await userEvent.type(input, '55.00');
 
     await editNewField(container, 'category');
-    const splitButton = document.body.querySelector(
-      '[data-testid="autocomplete"] [data-testid="split-transaction-button"]',
-    );
-    await userEvent.click(splitButton);
+    await userEvent.click(screen.getByTestId('split-transaction-button'));
     await waitForAutocomplete();
     await waitForAutocomplete();
     await waitForAutocomplete();
@@ -789,10 +800,6 @@ describe('Transactions', () => {
     }
 
     let input = await editField(container, 'category', 0);
-    const tooltip = container.querySelector('[data-testid="autocomplete"]');
-    const splitButton = tooltip.querySelector(
-      '[data-testid="split-transaction-button"]',
-    );
 
     // Make it clear that we are expected a negative transaction
     expect(getTransactions()[0].amount).toBe(-2777);
@@ -800,7 +807,7 @@ describe('Transactions', () => {
 
     // Make sure splitting a transaction works
     expect(getTransactions().length).toBe(5);
-    await userEvent.click(splitButton);
+    await userEvent.click(screen.getByTestId('split-transaction-button'));
     await waitForAutocomplete();
 
     expect(getTransactions().length).toBe(6);
@@ -910,17 +917,13 @@ describe('Transactions', () => {
     const { container, getTransactions } = renderTransactions();
 
     let input = await editField(container, 'category', 0);
-    const tooltip = container.querySelector('[data-testid="autocomplete"]');
-    const splitButton = tooltip.querySelector(
-      '[data-testid="split-transaction-button"',
-    );
 
     // The first transaction should always be a negative amount
     expect(getTransactions()[0].amount).toBe(-2777);
 
     // Add two new split transactions
     expect(getTransactions().length).toBe(5);
-    await userEvent.click(splitButton);
+    await userEvent.click(screen.getByTestId('split-transaction-button'));
     await waitForAutocomplete();
     await userEvent.click(
       container.querySelector('[data-testid="add-split-button"]'),
diff --git a/upcoming-release-notes/2631.md b/upcoming-release-notes/2631.md
new file mode 100644
index 000000000..bb664186d
--- /dev/null
+++ b/upcoming-release-notes/2631.md
@@ -0,0 +1,6 @@
+---
+category: Maintenance
+authors: [MatissJanis]
+---
+
+Migrating native `Tooltip` component to react-aria Tooltip/Popover (vol.3)
-- 
GitLab