From 64cd6ee3c9812bc6a467931129eb915064c59998 Mon Sep 17 00:00:00 2001
From: Matiss Janis Aboltins <matiss@mja.lv>
Date: Sun, 16 Jun 2024 14:31:10 +0100
Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20(tooltip)=20refactoring=20?=
 =?UTF-8?q?to=20react-aria=20(vol.9)=20(#2826)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../src/components/accounts/Header.jsx        | 145 +++++++++---------
 .../src/components/accounts/Reconcile.jsx     |  39 +++--
 .../src/components/common/HoverTarget.tsx     |  54 -------
 .../src/components/common/MenuTooltip.tsx     |  22 ---
 .../src/components/filters/FiltersMenu.jsx    |  35 ++---
 .../desktop-client/src/components/table.tsx   |  69 ++++-----
 upcoming-release-notes/2826.md                |   6 +
 7 files changed, 137 insertions(+), 233 deletions(-)
 delete mode 100644 packages/desktop-client/src/components/common/HoverTarget.tsx
 delete mode 100644 packages/desktop-client/src/components/common/MenuTooltip.tsx
 create mode 100644 upcoming-release-notes/2826.md

diff --git a/packages/desktop-client/src/components/accounts/Header.jsx b/packages/desktop-client/src/components/accounts/Header.jsx
index a3daf4b75..0ce452e8e 100644
--- a/packages/desktop-client/src/components/accounts/Header.jsx
+++ b/packages/desktop-client/src/components/accounts/Header.jsx
@@ -19,7 +19,7 @@ import { InitialFocus } from '../common/InitialFocus';
 import { Input } from '../common/Input';
 import { Menu } from '../common/Menu';
 import { MenuButton } from '../common/MenuButton';
-import { MenuTooltip } from '../common/MenuTooltip';
+import { Popover } from '../common/Popover';
 import { Search } from '../common/Search';
 import { Stack } from '../common/Stack';
 import { View } from '../common/View';
@@ -29,7 +29,7 @@ import { NotesButton } from '../NotesButton';
 import { SelectedTransactionsButton } from '../transactions/SelectedTransactions';
 
 import { Balances } from './Balance';
-import { ReconcilingMessage, ReconcileTooltip } from './Reconcile';
+import { ReconcilingMessage, ReconcileMenu } from './Reconcile';
 
 export function AccountHeader({
   filteredAmount,
@@ -86,6 +86,7 @@ export function AccountHeader({
 }) {
   const [menuOpen, setMenuOpen] = useState(false);
   const searchInput = useRef(null);
+  const triggerRef = useRef(null);
   const splitsExpanded = useSplitsExpanded();
   const syncServerStatus = useSyncServerStatus();
   const isUsingServer = syncServerStatus !== 'no-server';
@@ -338,9 +339,14 @@ export function AccountHeader({
           </Button>
           {account ? (
             <View>
-              <MenuButton onClick={() => setMenuOpen(true)} />
+              <MenuButton ref={triggerRef} onClick={() => setMenuOpen(true)} />
 
-              {menuOpen && (
+              <Popover
+                triggerRef={triggerRef}
+                style={{ width: 275 }}
+                isOpen={menuOpen}
+                onOpenChange={() => setMenuOpen(false)}
+              >
                 <AccountMenu
                   account={account}
                   canSync={canSync}
@@ -356,22 +362,31 @@ export function AccountHeader({
                   onReconcile={onReconcile}
                   onClose={() => setMenuOpen(false)}
                 />
-              )}
+              </Popover>
             </View>
           ) : (
             <View>
-              <MenuButton onClick={() => setMenuOpen(true)} />
+              <MenuButton ref={triggerRef} onClick={() => setMenuOpen(true)} />
 
-              {menuOpen && (
-                <CategoryMenu
+              <Popover
+                triggerRef={triggerRef}
+                isOpen={menuOpen}
+                onOpenChange={() => setMenuOpen(false)}
+              >
+                <Menu
                   onMenuSelect={item => {
                     setMenuOpen(false);
                     onMenuSelect(item);
                   }}
-                  onClose={() => setMenuOpen(false)}
-                  isSorted={isSorted}
+                  items={[
+                    isSorted && {
+                      name: 'remove-sorting',
+                      text: 'Remove all sorting',
+                    },
+                    { name: 'export', text: 'Export' },
+                  ]}
                 />
-              )}
+              </Popover>
             </View>
           )}
         </Stack>
@@ -418,76 +433,54 @@ function AccountMenu({
   const syncServerStatus = useSyncServerStatus();
 
   return tooltip === 'reconcile' ? (
-    <ReconcileTooltip
+    <ReconcileMenu
       account={account}
       onClose={onClose}
       onReconcile={onReconcile}
     />
   ) : (
-    <MenuTooltip width={200} onClose={onClose}>
-      <Menu
-        onMenuSelect={item => {
-          if (item === 'reconcile') {
-            setTooltip('reconcile');
-          } else {
-            onMenuSelect(item);
-          }
-        }}
-        items={[
-          isSorted && {
-            name: 'remove-sorting',
-            text: 'Remove all sorting',
-          },
-          canShowBalances && {
-            name: 'toggle-balance',
-            text: (showBalances ? 'Hide' : 'Show') + ' running balance',
-          },
-          {
-            name: 'toggle-cleared',
-            text: (showCleared ? 'Hide' : 'Show') + ' “cleared” checkboxes',
-          },
-          {
-            name: 'toggle-reconciled',
-            text:
-              (showReconciled ? 'Hide' : 'Show') + ' reconciled transactions',
-          },
-          { name: 'export', text: 'Export' },
-          { name: 'reconcile', text: 'Reconcile' },
-          account &&
-            !account.closed &&
-            (canSync
-              ? {
-                  name: 'unlink',
-                  text: 'Unlink account',
-                }
-              : syncServerStatus === 'online' && {
-                  name: 'link',
-                  text: 'Link account',
-                }),
-          account.closed
-            ? { name: 'reopen', text: 'Reopen account' }
-            : { name: 'close', text: 'Close account' },
-        ].filter(x => x)}
-      />
-    </MenuTooltip>
-  );
-}
-
-function CategoryMenu({ onClose, onMenuSelect, isSorted }) {
-  return (
-    <MenuTooltip width={200} onClose={onClose}>
-      <Menu
-        onMenuSelect={item => {
+    <Menu
+      onMenuSelect={item => {
+        if (item === 'reconcile') {
+          setTooltip('reconcile');
+        } else {
           onMenuSelect(item);
-        }}
-        items={[
-          isSorted && {
-            name: 'remove-sorting',
-            text: 'Remove all sorting',
-          },
-          { name: 'export', text: 'Export' },
-        ]}
-      />
-    </MenuTooltip>
+        }
+      }}
+      items={[
+        isSorted && {
+          name: 'remove-sorting',
+          text: 'Remove all sorting',
+        },
+        canShowBalances && {
+          name: 'toggle-balance',
+          text: (showBalances ? 'Hide' : 'Show') + ' running balance',
+        },
+        {
+          name: 'toggle-cleared',
+          text: (showCleared ? 'Hide' : 'Show') + ' “cleared” checkboxes',
+        },
+        {
+          name: 'toggle-reconciled',
+          text: (showReconciled ? 'Hide' : 'Show') + ' reconciled transactions',
+        },
+        { name: 'export', text: 'Export' },
+        { name: 'reconcile', text: 'Reconcile' },
+        account &&
+          !account.closed &&
+          (canSync
+            ? {
+                name: 'unlink',
+                text: 'Unlink account',
+              }
+            : syncServerStatus === 'online' && {
+                name: 'link',
+                text: 'Link account',
+              }),
+        account.closed
+          ? { name: 'reopen', text: 'Reopen account' }
+          : { name: 'close', text: 'Close account' },
+      ].filter(x => x)}
+    />
   );
 }
diff --git a/packages/desktop-client/src/components/accounts/Reconcile.jsx b/packages/desktop-client/src/components/accounts/Reconcile.jsx
index 76fc01da3..d80668b8e 100644
--- a/packages/desktop-client/src/components/accounts/Reconcile.jsx
+++ b/packages/desktop-client/src/components/accounts/Reconcile.jsx
@@ -12,7 +12,6 @@ import { Text } from '../common/Text';
 import { View } from '../common/View';
 import { useFormat } from '../spreadsheet/useFormat';
 import { useSheetValue } from '../spreadsheet/useSheetValue';
-import { Tooltip } from '../tooltips';
 
 export function ReconcilingMessage({
   balanceQuery,
@@ -95,7 +94,7 @@ export function ReconcilingMessage({
   );
 }
 
-export function ReconcileTooltip({ account, onReconcile, onClose }) {
+export function ReconcileMenu({ account, onReconcile, onClose }) {
   const balanceQuery = queries.accountBalance(account);
   const clearedBalance = useSheetValue({
     name: balanceQuery.name + '-cleared',
@@ -117,24 +116,22 @@ export function ReconcileTooltip({ account, onReconcile, onClose }) {
   }
 
   return (
-    <Tooltip position="bottom-right" width={275} onClose={onClose}>
-      <View style={{ padding: '5px 8px' }}>
-        <Text>
-          Enter the current balance of your bank account that you want to
-          reconcile with:
-        </Text>
-        <form onSubmit={onSubmit}>
-          {clearedBalance != null && (
-            <InitialFocus>
-              <Input
-                defaultValue={format(clearedBalance, 'financial')}
-                style={{ margin: '7px 0' }}
-              />
-            </InitialFocus>
-          )}
-          <Button type="primary">Reconcile</Button>
-        </form>
-      </View>
-    </Tooltip>
+    <View style={{ padding: '5px 8px' }}>
+      <Text>
+        Enter the current balance of your bank account that you want to
+        reconcile with:
+      </Text>
+      <form onSubmit={onSubmit}>
+        {clearedBalance != null && (
+          <InitialFocus>
+            <Input
+              defaultValue={format(clearedBalance, 'financial')}
+              style={{ margin: '7px 0' }}
+            />
+          </InitialFocus>
+        )}
+        <Button type="primary">Reconcile</Button>
+      </form>
+    </View>
   );
 }
diff --git a/packages/desktop-client/src/components/common/HoverTarget.tsx b/packages/desktop-client/src/components/common/HoverTarget.tsx
deleted file mode 100644
index ec3d4e015..000000000
--- a/packages/desktop-client/src/components/common/HoverTarget.tsx
+++ /dev/null
@@ -1,54 +0,0 @@
-import { useCallback, useEffect, useState, type ReactNode } from 'react';
-
-import { type CSSProperties } from '../../style';
-
-import { View } from './View';
-
-type HoverTargetProps = {
-  style?: CSSProperties;
-  contentStyle?: CSSProperties;
-  children: ReactNode;
-  renderContent: () => ReactNode;
-  disabled?: boolean;
-};
-
-export function HoverTarget({
-  style,
-  contentStyle,
-  children,
-  renderContent,
-  disabled,
-}: HoverTargetProps) {
-  const [hovered, setHovered] = useState(false);
-
-  const onPointerEnter = useCallback(() => {
-    if (!disabled) {
-      setHovered(true);
-    }
-  }, [disabled]);
-
-  const onPointerLeave = useCallback(() => {
-    if (!disabled) {
-      setHovered(false);
-    }
-  }, [disabled]);
-
-  useEffect(() => {
-    if (disabled && hovered) {
-      setHovered(false);
-    }
-  }, [disabled, hovered]);
-
-  return (
-    <View style={style}>
-      <View
-        onPointerEnter={onPointerEnter}
-        onPointerLeave={onPointerLeave}
-        style={contentStyle}
-      >
-        {children}
-      </View>
-      {hovered && renderContent()}
-    </View>
-  );
-}
diff --git a/packages/desktop-client/src/components/common/MenuTooltip.tsx b/packages/desktop-client/src/components/common/MenuTooltip.tsx
deleted file mode 100644
index 2b255d77f..000000000
--- a/packages/desktop-client/src/components/common/MenuTooltip.tsx
+++ /dev/null
@@ -1,22 +0,0 @@
-import React, { type ReactNode } from 'react';
-
-import { Tooltip } from '../tooltips';
-
-type MenuTooltipProps = {
-  width: number;
-  onClose: () => void;
-  children: ReactNode;
-};
-
-export function MenuTooltip({ width, onClose, children }: MenuTooltipProps) {
-  return (
-    <Tooltip
-      position="bottom-right"
-      width={width}
-      style={{ padding: 0 }}
-      onClose={onClose}
-    >
-      {children}
-    </Tooltip>
-  );
-}
diff --git a/packages/desktop-client/src/components/filters/FiltersMenu.jsx b/packages/desktop-client/src/components/filters/FiltersMenu.jsx
index b8b79edae..ac1f48d6a 100644
--- a/packages/desktop-client/src/components/filters/FiltersMenu.jsx
+++ b/packages/desktop-client/src/components/filters/FiltersMenu.jsx
@@ -21,16 +21,15 @@ import {
 import { titleFirst } from 'loot-core/src/shared/util';
 
 import { useDateFormat } from '../../hooks/useDateFormat';
-import { theme } from '../../style';
+import { styles, 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';
+import { Tooltip } from '../common/Tooltip';
 import { View } from '../common/View';
-import { Tooltip } from '../tooltips';
 import { GenericInput } from '../util/GenericInput';
 
 import { CompactFiltersButton } from './CompactFiltersButton';
@@ -321,23 +320,17 @@ export function FilterButton({ onApply, compact, hover, exclude }) {
   return (
     <View>
       <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>
-            )
-          }
+        <Tooltip
+          style={{
+            ...styles.tooltip,
+            lineHeight: 1.5,
+            padding: '6px 10px',
+          }}
+          content={<Text>Filters</Text>}
+          placement="bottom start"
+          triggerProps={{
+            isDisabled: !hover,
+          }}
         >
           {compact ? (
             <CompactFiltersButton
@@ -346,7 +339,7 @@ export function FilterButton({ onApply, compact, hover, exclude }) {
           ) : (
             <FiltersButton onClick={() => dispatch({ type: 'select-field' })} />
           )}
-        </HoverTarget>
+        </Tooltip>
       </View>
 
       <Popover
diff --git a/packages/desktop-client/src/components/table.tsx b/packages/desktop-client/src/components/table.tsx
index cfd2ad90e..d321e6052 100644
--- a/packages/desktop-client/src/components/table.tsx
+++ b/packages/desktop-client/src/components/table.tsx
@@ -31,6 +31,7 @@ import { type CSSProperties, styles, theme } from '../style';
 import { Button } from './common/Button';
 import { Input } from './common/Input';
 import { Menu } from './common/Menu';
+import { Popover } from './common/Popover';
 import { Text } from './common/Text';
 import { View } from './common/View';
 import { FixedSizeList } from './FixedSizeList';
@@ -41,7 +42,7 @@ import {
 import { type Binding } from './spreadsheet';
 import { type FormatType, useFormat } from './spreadsheet/useFormat';
 import { useSheetValue } from './spreadsheet/useSheetValue';
-import { Tooltip, IntersectionBoundary } from './tooltips';
+import { IntersectionBoundary } from './tooltips';
 
 export const ROW_HEIGHT = 32;
 
@@ -383,38 +384,24 @@ type InputCellProps = ComponentProps<typeof Cell> & {
   onUpdate?: ComponentProps<typeof InputValue>['onUpdate'];
   onBlur?: ComponentProps<typeof InputValue>['onBlur'];
   textAlign?: CSSProperties['textAlign'];
-  error?: ReactNode;
 };
 export function InputCell({
   inputProps,
   onUpdate,
   onBlur,
   textAlign,
-  error,
   ...props
 }: InputCellProps) {
   return (
     <Cell textAlign={textAlign} {...props}>
       {() => (
-        <>
-          <InputValue
-            value={props.value}
-            onUpdate={onUpdate}
-            onBlur={onBlur}
-            style={{ textAlign, ...(inputProps && inputProps.style) }}
-            {...inputProps}
-          />
-          {error && (
-            <Tooltip
-              key="error"
-              targetHeight={ROW_HEIGHT}
-              width={180}
-              position="bottom-left"
-            >
-              {error}
-            </Tooltip>
-          )}
-        </>
+        <InputValue
+          value={props.value}
+          onUpdate={onUpdate}
+          onBlur={onBlur}
+          style={{ textAlign, ...(inputProps && inputProps.style) }}
+          {...inputProps}
+        />
       )}
     </Cell>
   );
@@ -809,6 +796,7 @@ export function TableHeader({
 export function SelectedItemsButton({ name, items, onSelect }) {
   const selectedItems = useSelectedItems();
   const [menuOpen, setMenuOpen] = useState(null);
+  const triggerRef = useRef(null);
 
   if (selectedItems.size === 0) {
     return null;
@@ -817,6 +805,7 @@ export function SelectedItemsButton({ name, items, onSelect }) {
   return (
     <View style={{ marginLeft: 10, flexShrink: 0 }}>
       <Button
+        ref={triggerRef}
         type="bare"
         style={{ color: theme.pageTextPositive }}
         onClick={() => setMenuOpen(true)}
@@ -830,23 +819,25 @@ export function SelectedItemsButton({ name, items, onSelect }) {
         {selectedItems.size} {name}
       </Button>
 
-      {menuOpen && (
-        <Tooltip
-          position="bottom-right"
-          width={200}
-          style={{ padding: 0, backgroundColor: theme.menuBackground }}
-          onClose={() => setMenuOpen(false)}
-          data-testid={name + '-select-tooltip'}
-        >
-          <Menu
-            onMenuSelect={name => {
-              onSelect(name, [...selectedItems]);
-              setMenuOpen(false);
-            }}
-            items={items}
-          />
-        </Tooltip>
-      )}
+      <Popover
+        triggerRef={triggerRef}
+        style={{
+          width: 200,
+          padding: 0,
+          backgroundColor: theme.menuBackground,
+        }}
+        isOpen={menuOpen}
+        onOpenChange={() => setMenuOpen(false)}
+        data-testid={name + '-select-tooltip'}
+      >
+        <Menu
+          onMenuSelect={name => {
+            onSelect(name, [...selectedItems]);
+            setMenuOpen(false);
+          }}
+          items={items}
+        />
+      </Popover>
     </View>
   );
 }
diff --git a/upcoming-release-notes/2826.md b/upcoming-release-notes/2826.md
new file mode 100644
index 000000000..4ca032999
--- /dev/null
+++ b/upcoming-release-notes/2826.md
@@ -0,0 +1,6 @@
+---
+category: Maintenance
+authors: [MatissJanis]
+---
+
+Migrating native `Tooltip` component to react-aria Tooltip/Popover (vol.9)
-- 
GitLab