diff --git a/packages/desktop-client/src/components/manager/BudgetList.jsx b/packages/desktop-client/src/components/manager/BudgetList.jsx index 912a68fc4dd3090ebcc07502e44d01841d37505a..b012bdc06a0b3e10c53184b2ae7f1bee0ca095bd 100644 --- a/packages/desktop-client/src/components/manager/BudgetList.jsx +++ b/packages/desktop-client/src/components/manager/BudgetList.jsx @@ -28,9 +28,9 @@ import { styles, theme } from '../../style'; import { tokens } from '../../tokens'; import { Button } from '../common/Button'; import { Menu } from '../common/Menu'; +import { Popover } from '../common/Popover'; import { Text } from '../common/Text'; import { View } from '../common/View'; -import { Tooltip } from '../tooltips'; function getFileDescription(file) { if (file.state === 'unknown') { @@ -84,11 +84,13 @@ function FileMenu({ onDelete, onClose }) { } function FileMenuButton({ state, onDelete }) { + const triggerRef = useRef(null); const [menuOpen, setMenuOpen] = useState(false); return ( <View> <Button + ref={triggerRef} type="bare" aria-label="Menu" onClick={e => { @@ -98,19 +100,18 @@ function FileMenuButton({ state, onDelete }) { > <SvgDotsHorizontalTriple style={{ width: 16, height: 16 }} /> </Button> - {menuOpen && ( - <Tooltip - position="bottom-right" - style={{ padding: 0 }} + + <Popover + triggerRef={triggerRef} + isOpen={menuOpen} + onOpenChange={() => setMenuOpen(false)} + > + <FileMenu + state={state} + onDelete={onDelete} onClose={() => setMenuOpen(false)} - > - <FileMenu - state={state} - onDelete={onDelete} - onClose={() => setMenuOpen(false)} - /> - </Tooltip> - )} + /> + </Popover> </View> ); } diff --git a/packages/desktop-client/src/components/modals/AccountMenuModal.tsx b/packages/desktop-client/src/components/modals/AccountMenuModal.tsx index 4c44e1a75f48f030b1b6207f43335502571dc13d..ecc87e1db6f3e27bfedad1047d60c179eaaf83e0 100644 --- a/packages/desktop-client/src/components/modals/AccountMenuModal.tsx +++ b/packages/desktop-client/src/components/modals/AccountMenuModal.tsx @@ -1,4 +1,4 @@ -import React, { type ComponentProps, useState } from 'react'; +import React, { type ComponentProps, useRef, useState } from 'react'; import { type AccountEntity } from 'loot-core/types/models'; @@ -10,10 +10,10 @@ import { type CSSProperties, styles, theme } from '../../style'; import { Button } from '../common/Button'; import { Menu } from '../common/Menu'; import { Modal, ModalTitle } from '../common/Modal'; +import { Popover } from '../common/Popover'; import { View } from '../common/View'; import { type CommonModalProps } from '../Modals'; import { Notes } from '../Notes'; -import { Tooltip } from '../tooltips'; type AccountMenuModalProps = { modalProps: CommonModalProps; @@ -155,6 +155,7 @@ function AdditionalAccountMenu({ onClose, onReopen, }: AdditionalAccountMenuProps) { + const triggerRef = useRef(null); const [menuOpen, setMenuOpen] = useState(false); const itemStyle: CSSProperties = { ...styles.mediumText, @@ -169,6 +170,7 @@ function AdditionalAccountMenu({ return ( <View> <Button + ref={triggerRef} type="bare" aria-label="Menu" onClick={() => { @@ -180,47 +182,44 @@ function AdditionalAccountMenu({ height={17} style={{ color: 'currentColor' }} /> - {menuOpen && ( - <Tooltip - position="bottom-left" - style={{ padding: 0 }} - onClose={() => { + <Popover + triggerRef={triggerRef} + isOpen={menuOpen} + placement="bottom start" + onOpenChange={() => setMenuOpen(false)} + > + <Menu + getItemStyle={getItemStyle} + items={[ + account.closed + ? { + name: 'reopen', + text: 'Reopen account', + icon: SvgLockOpen, + iconSize: 15, + } + : { + name: 'close', + text: 'Close account', + icon: SvgClose, + iconSize: 15, + }, + ]} + onMenuSelect={name => { setMenuOpen(false); + switch (name) { + case 'close': + onClose?.(account.id); + break; + case 'reopen': + onReopen?.(account.id); + break; + default: + throw new Error(`Unrecognized menu option: ${name}`); + } }} - > - <Menu - getItemStyle={getItemStyle} - items={[ - account.closed - ? { - name: 'reopen', - text: 'Reopen account', - icon: SvgLockOpen, - iconSize: 15, - } - : { - name: 'close', - text: 'Close account', - icon: SvgClose, - iconSize: 15, - }, - ]} - onMenuSelect={name => { - setMenuOpen(false); - switch (name) { - case 'close': - onClose?.(account.id); - break; - case 'reopen': - onReopen?.(account.id); - break; - default: - throw new Error(`Unrecognized menu option: ${name}`); - } - }} - /> - </Tooltip> - )} + /> + </Popover> </Button> </View> ); diff --git a/packages/desktop-client/src/components/modals/CategoryGroupMenuModal.tsx b/packages/desktop-client/src/components/modals/CategoryGroupMenuModal.tsx index aab94588eca56b738b246d6135cac01ef0a5bf56..776d134d4cdc3c0b4dc17cb625ddc07e2173c414 100644 --- a/packages/desktop-client/src/components/modals/CategoryGroupMenuModal.tsx +++ b/packages/desktop-client/src/components/modals/CategoryGroupMenuModal.tsx @@ -1,5 +1,5 @@ // @ts-strict-ignore -import React, { type ComponentProps, useState } from 'react'; +import React, { type ComponentProps, useRef, useState } from 'react'; import { type CategoryGroupEntity } from 'loot-core/src/types/models'; @@ -11,10 +11,10 @@ import { type CSSProperties, styles, theme } from '../../style'; import { Button } from '../common/Button'; import { Menu } from '../common/Menu'; import { Modal, ModalTitle } from '../common/Modal'; +import { Popover } from '../common/Popover'; import { View } from '../common/View'; import { type CommonModalProps } from '../Modals'; import { Notes } from '../Notes'; -import { Tooltip } from '../tooltips'; type CategoryGroupMenuModalProps = { modalProps: CommonModalProps; @@ -155,6 +155,7 @@ export function CategoryGroupMenuModal({ } function AdditionalCategoryGroupMenu({ group, onDelete, onToggleVisibility }) { + const triggerRef = useRef(null); const [menuOpen, setMenuOpen] = useState(false); const itemStyle: CSSProperties = { ...styles.mediumText, @@ -170,6 +171,7 @@ function AdditionalCategoryGroupMenu({ group, onDelete, onToggleVisibility }) { <View> {!group.is_income && ( <Button + ref={triggerRef} type="bare" aria-label="Menu" onClick={() => { @@ -181,52 +183,47 @@ function AdditionalCategoryGroupMenu({ group, onDelete, onToggleVisibility }) { height={17} style={{ color: 'currentColor' }} /> - {menuOpen && ( - <Tooltip - position="bottom-left" - style={{ padding: 0 }} - onClose={() => { - setMenuOpen(false); + <Popover + triggerRef={triggerRef} + isOpen={menuOpen} + placement="bottom start" + onOpenChange={() => setMenuOpen(false)} + > + <Menu + style={{ + ...styles.mediumText, + color: theme.formLabelText, }} - > - <Menu - style={{ - ...styles.mediumText, - color: theme.formLabelText, - }} - getItemStyle={getItemStyle} - items={ - [ + getItemStyle={getItemStyle} + items={ + [ + { + name: 'toggleVisibility', + text: group.hidden ? 'Show' : 'Hide', + icon: group.hidden ? SvgViewShow : SvgViewHide, + iconSize: 16, + }, + ...(!group.is_income && [ + Menu.line, { - name: 'toggleVisibility', - text: group.hidden ? 'Show' : 'Hide', - icon: group.hidden ? SvgViewShow : SvgViewHide, - iconSize: 16, + name: 'delete', + text: 'Delete', + icon: SvgTrash, + iconSize: 15, }, - ...(!group.is_income && [ - Menu.line, - { - name: 'delete', - text: 'Delete', - icon: SvgTrash, - iconSize: 15, - }, - ]), - ].filter(i => i != null) as ComponentProps< - typeof Menu - >['items'] + ]), + ].filter(i => i != null) as ComponentProps<typeof Menu>['items'] + } + onMenuSelect={itemName => { + setMenuOpen(false); + if (itemName === 'delete') { + onDelete(); + } else if (itemName === 'toggleVisibility') { + onToggleVisibility(); } - onMenuSelect={itemName => { - setMenuOpen(false); - if (itemName === 'delete') { - onDelete(); - } else if (itemName === 'toggleVisibility') { - onToggleVisibility(); - } - }} - /> - </Tooltip> - )} + }} + /> + </Popover> </Button> )} </View> diff --git a/packages/desktop-client/src/components/modals/CategoryMenuModal.tsx b/packages/desktop-client/src/components/modals/CategoryMenuModal.tsx index 1fa27a5cdfb7a892d47ab53b9826f46197c272fc..f56161a4529fba2af6f4891cb8c4d6f5e9ba7e4a 100644 --- a/packages/desktop-client/src/components/modals/CategoryMenuModal.tsx +++ b/packages/desktop-client/src/components/modals/CategoryMenuModal.tsx @@ -1,5 +1,5 @@ // @ts-strict-ignore -import React, { useState } from 'react'; +import React, { useRef, useState } from 'react'; import { type CategoryEntity } from 'loot-core/src/types/models'; @@ -12,10 +12,10 @@ import { type CSSProperties, styles, theme } from '../../style'; import { Button } from '../common/Button'; import { Menu } from '../common/Menu'; import { Modal, ModalTitle } from '../common/Modal'; +import { Popover } from '../common/Popover'; import { View } from '../common/View'; import { type CommonModalProps } from '../Modals'; import { Notes } from '../Notes'; -import { Tooltip } from '../tooltips'; type CategoryMenuModalProps = { modalProps: CommonModalProps; @@ -147,6 +147,7 @@ function AdditionalCategoryMenu({ onDelete, onToggleVisibility, }) { + const triggerRef = useRef(null); const [menuOpen, setMenuOpen] = useState(false); const itemStyle: CSSProperties = { ...styles.mediumText, @@ -161,6 +162,7 @@ function AdditionalCategoryMenu({ return ( <View> <Button + ref={triggerRef} type="bare" aria-label="Menu" onClick={() => { @@ -172,42 +174,39 @@ function AdditionalCategoryMenu({ height={17} style={{ color: 'currentColor' }} /> - {menuOpen && ( - <Tooltip - position="bottom-left" - style={{ padding: 0 }} - onClose={() => { + <Popover + triggerRef={triggerRef} + isOpen={menuOpen} + placement="bottom start" + onOpenChange={() => setMenuOpen(false)} + > + <Menu + getItemStyle={getItemStyle} + items={[ + !categoryGroup?.hidden && { + name: 'toggleVisibility', + text: category.hidden ? 'Show' : 'Hide', + icon: category.hidden ? SvgViewShow : SvgViewHide, + iconSize: 16, + }, + !categoryGroup?.hidden && Menu.line, + { + name: 'delete', + text: 'Delete', + icon: SvgTrash, + iconSize: 15, + }, + ]} + onMenuSelect={itemName => { setMenuOpen(false); + if (itemName === 'delete') { + onDelete(); + } else if (itemName === 'toggleVisibility') { + onToggleVisibility(); + } }} - > - <Menu - getItemStyle={getItemStyle} - items={[ - !categoryGroup?.hidden && { - name: 'toggleVisibility', - text: category.hidden ? 'Show' : 'Hide', - icon: category.hidden ? SvgViewShow : SvgViewHide, - iconSize: 16, - }, - !categoryGroup?.hidden && Menu.line, - { - name: 'delete', - text: 'Delete', - icon: SvgTrash, - iconSize: 15, - }, - ]} - onMenuSelect={itemName => { - setMenuOpen(false); - if (itemName === 'delete') { - onDelete(); - } else if (itemName === 'toggleVisibility') { - onToggleVisibility(); - } - }} - /> - </Tooltip> - )} + /> + </Popover> </Button> </View> ); diff --git a/packages/desktop-client/src/components/modals/EditRule.jsx b/packages/desktop-client/src/components/modals/EditRule.jsx index cb3c6d3ac60a1f135ab826cf9a1ea67a199c977c..12ed1a593509f240925a6fa952455ce6a3c385d3 100644 --- a/packages/desktop-client/src/components/modals/EditRule.jsx +++ b/packages/desktop-client/src/components/modals/EditRule.jsx @@ -40,9 +40,9 @@ import { Modal } from '../common/Modal'; 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 { StatusBadge } from '../schedules/StatusBadge'; -import { Tooltip } from '../tooltips'; import { SimpleTransactionsTable } from '../transactions/SimpleTransactionsTable'; import { BetweenAmountInput } from '../util/AmountInput'; import { DisplayId } from '../util/DisplayId'; @@ -415,33 +415,29 @@ function ActionEditor({ action, editorStyle, onChange, onDelete, onAdd }) { } function StageInfo() { - const [open, setOpen] = useState(); - return ( <View style={{ position: 'relative', marginLeft: 5 }}> - <View - onMouseEnter={() => setOpen(true)} - onMouseLeave={() => setOpen(false)} + <Tooltip + content={ + <> + The stage of a rule allows you to force a specific order. Pre rules + always run first, and post rules always run last. Within each stage + rules are automatically ordered from least to most specific. + </> + } + placement="bottom start" + style={{ + ...styles.tooltip, + padding: 10, + color: theme.pageTextLight, + maxWidth: 450, + lineHeight: 1.5, + }} > <SvgInformationOutline style={{ width: 11, height: 11, color: theme.pageTextLight }} /> - </View> - {open && ( - <Tooltip - position="bottom-left" - style={{ - padding: 10, - color: theme.pageTextLight, - maxWidth: 450, - lineHeight: 1.5, - }} - > - The stage of a rule allows you to force a specific order. Pre rules - always run first, and post rules always run last. Within each stage - rules are automatically ordered from least to most specific. - </Tooltip> - )} + </Tooltip> </View> ); } diff --git a/packages/desktop-client/src/components/modals/GoCardlessExternalMsg.tsx b/packages/desktop-client/src/components/modals/GoCardlessExternalMsg.tsx index 5de7b9fde9ea2361afd7edc93eb60684509145a8..a7381d4418dd1c9544bda7a5126eed8989fd9692 100644 --- a/packages/desktop-client/src/components/modals/GoCardlessExternalMsg.tsx +++ b/packages/desktop-client/src/components/modals/GoCardlessExternalMsg.tsx @@ -20,10 +20,10 @@ import { Link } from '../common/Link'; import { Menu } from '../common/Menu'; import { Modal } from '../common/Modal'; import { Paragraph } from '../common/Paragraph'; +import { Popover } from '../common/Popover'; import { View } from '../common/View'; import { FormField, FormLabel } from '../forms'; import { type CommonModalProps } from '../Modals'; -import { Tooltip } from '../tooltips'; import { COUNTRY_OPTIONS } from './countries'; @@ -103,6 +103,7 @@ export function GoCardlessExternalMsg({ >(null); const [menuOpen, setMenuOpen] = useState<boolean>(false); const data = useRef<GoCardlessToken | null>(null); + const triggerRef = useRef(null); const { data: bankOptions, @@ -230,6 +231,7 @@ export function GoCardlessExternalMsg({ Link bank in browser → </Button> <Button + ref={triggerRef} type="bare" onClick={() => setMenuOpen(true)} aria-label="Menu" @@ -239,28 +241,27 @@ export function GoCardlessExternalMsg({ height={15} style={{ transform: 'rotateZ(90deg)' }} /> - {menuOpen && ( - <Tooltip - position="bottom-right" - width={200} - style={{ padding: 0 }} - onClose={() => setMenuOpen(false)} - > - <Menu - onMenuSelect={item => { - if (item === 'reconfigure') { - onGoCardlessInit(); - } - }} - items={[ - { - name: 'reconfigure', - text: 'Set new API secrets', - }, - ]} - /> - </Tooltip> - )} + + <Popover + triggerRef={triggerRef} + isOpen={menuOpen} + style={{ width: 200 }} + onOpenChange={() => setMenuOpen(false)} + > + <Menu + onMenuSelect={item => { + if (item === 'reconfigure') { + onGoCardlessInit(); + } + }} + items={[ + { + name: 'reconfigure', + text: 'Set new API secrets', + }, + ]} + /> + </Popover> </Button> </View> </View> diff --git a/packages/desktop-client/src/components/payees/ManagePayees.jsx b/packages/desktop-client/src/components/payees/ManagePayees.jsx index da15bb8593dca65ab31526c9cde01a8932326461..79449a6cb776c27b82b02854f261ce3a0c440f9d 100644 --- a/packages/desktop-client/src/components/payees/ManagePayees.jsx +++ b/packages/desktop-client/src/components/payees/ManagePayees.jsx @@ -23,6 +23,7 @@ import { useStableCallback } from '../../hooks/useStableCallback'; import { SvgExpandArrow } from '../../icons/v0'; import { theme } from '../../style'; import { Button } from '../common/Button'; +import { Popover } from '../common/Popover'; import { Search } from '../common/Search'; import { View } from '../common/View'; import { TableHeader, Cell, SelectCell, useTableNavigator } from '../table'; @@ -102,6 +103,7 @@ export const ManagePayees = forwardRef( const [filter, setFilter] = useState(''); const table = useRef(null); const scrollTo = useRef(null); + const triggerRef = useRef(null); const resetAnimation = useRef(false); const [orphanedOnly, setOrphanedOnly] = useState(false); @@ -233,6 +235,7 @@ export const ManagePayees = forwardRef( > <View style={{ flexShrink: 0 }}> <Button + ref={triggerRef} type="bare" style={{ marginRight: 10 }} disabled={buttonsDisabled} @@ -245,7 +248,14 @@ export const ManagePayees = forwardRef( plural(selected.items.size, 'payee', 'payees')} <SvgExpandArrow width={8} height={8} style={{ marginLeft: 5 }} /> </Button> - {menuOpen && ( + + <Popover + triggerRef={triggerRef} + isOpen={menuOpen} + placement="bottom start" + style={{ width: 250 }} + onOpenChange={() => setMenuOpen(false)} + > <PayeeMenu payeesById={payeesById} selectedPayees={selected.items} @@ -253,7 +263,7 @@ export const ManagePayees = forwardRef( onDelete={onDelete} onMerge={onMerge} /> - )} + </Popover> </View> <View style={{ diff --git a/packages/desktop-client/src/components/payees/PayeeMenu.tsx b/packages/desktop-client/src/components/payees/PayeeMenu.tsx index 2a4867a669a1b499061f7c83d5f769cc95bf88ac..380f13ccaa57324b171a0beffc0b25c865a5c5ff 100644 --- a/packages/desktop-client/src/components/payees/PayeeMenu.tsx +++ b/packages/desktop-client/src/components/payees/PayeeMenu.tsx @@ -4,7 +4,6 @@ import { SvgDelete, SvgMerge } from '../../icons/v0'; import { theme } from '../../style'; import { Menu } from '../common/Menu'; import { View } from '../common/View'; -import { Tooltip } from '../tooltips'; type PayeeMenuProps = { payeesById: Record<PayeeEntity['id'], PayeeEntity>; @@ -27,57 +26,50 @@ export function PayeeMenu({ ); return ( - <Tooltip - position="bottom" - width={250} - style={{ padding: 0 }} - onClose={onClose} - > - <Menu - onMenuSelect={type => { - onClose(); - switch (type) { - case 'delete': - onDelete(); - break; - case 'merge': - onMerge(); - break; - default: - } - }} - footer={ - <View - style={{ - padding: 3, - fontSize: 11, - fontStyle: 'italic', - color: theme.pageTextSubdued, - }} - > - {[...selectedPayees] - .slice(0, 4) - .map(id => payeesById[id].name) - .join(', ') + (selectedPayees.size > 4 ? ', and more' : '')} - </View> + <Menu + onMenuSelect={type => { + onClose(); + switch (type) { + case 'delete': + onDelete(); + break; + case 'merge': + onMerge(); + break; + default: } - items={[ - { - icon: SvgDelete, - name: 'delete', - text: 'Delete', - disabled: isDisabled, - }, - { - icon: SvgMerge, - iconSize: 9, - name: 'merge', - text: 'Merge', - disabled: isDisabled || selectedPayees.size < 2, - }, - Menu.line, - ]} - /> - </Tooltip> + }} + footer={ + <View + style={{ + padding: 3, + fontSize: 11, + fontStyle: 'italic', + color: theme.pageTextSubdued, + }} + > + {[...selectedPayees] + .slice(0, 4) + .map(id => payeesById[id].name) + .join(', ') + (selectedPayees.size > 4 ? ', and more' : '')} + </View> + } + items={[ + { + icon: SvgDelete, + name: 'delete', + text: 'Delete', + disabled: isDisabled, + }, + { + icon: SvgMerge, + iconSize: 9, + name: 'merge', + text: 'Merge', + disabled: isDisabled || selectedPayees.size < 2, + }, + Menu.line, + ]} + /> ); } diff --git a/packages/desktop-client/src/components/schedules/SchedulesTable.tsx b/packages/desktop-client/src/components/schedules/SchedulesTable.tsx index 20cf75e4dbdf9ff740d3fca408470c23f9d70f67..a3b2edad3673b1f68a920efa3566dad91d8b0992 100644 --- a/packages/desktop-client/src/components/schedules/SchedulesTable.tsx +++ b/packages/desktop-client/src/components/schedules/SchedulesTable.tsx @@ -1,5 +1,5 @@ // @ts-strict-ignore -import React, { useState, useMemo, type CSSProperties } from 'react'; +import React, { useRef, useState, useMemo, type CSSProperties } from 'react'; import { type ScheduleStatusType, @@ -18,11 +18,11 @@ import { SvgCheck } from '../../icons/v2'; import { theme } from '../../style'; import { Button } from '../common/Button'; import { Menu } from '../common/Menu'; +import { Popover } from '../common/Popover'; import { Text } from '../common/Text'; import { View } from '../common/View'; import { PrivacyFilter } from '../PrivacyFilter'; import { Table, TableHeader, Row, Field, Cell } from '../table'; -import { Tooltip } from '../tooltips'; import { DisplayId } from '../util/DisplayId'; import { StatusBadge } from './StatusBadge'; @@ -60,6 +60,7 @@ function OverflowMenu({ status: ScheduleStatusType; onAction: SchedulesTableProps['onAction']; }) { + const triggerRef = useRef(null); const [open, setOpen] = useState(false); const getMenuItems = () => { @@ -96,6 +97,7 @@ function OverflowMenu({ return ( <View> <Button + ref={triggerRef} type="bare" aria-label="Menu" onClick={e => { @@ -109,22 +111,20 @@ function OverflowMenu({ style={{ transform: 'rotateZ(90deg)' }} /> </Button> - {open && ( - <Tooltip - position="bottom-right" - width={150} - style={{ padding: 0 }} - onClose={() => setOpen(false)} - > - <Menu - onMenuSelect={name => { - onAction(name, schedule.id); - setOpen(false); - }} - items={getMenuItems()} - /> - </Tooltip> - )} + + <Popover + triggerRef={triggerRef} + isOpen={open} + onOpenChange={() => setOpen(false)} + > + <Menu + onMenuSelect={name => { + onAction(name, schedule.id); + setOpen(false); + }} + items={getMenuItems()} + /> + </Popover> </View> ); } diff --git a/upcoming-release-notes/2771.md b/upcoming-release-notes/2771.md new file mode 100644 index 0000000000000000000000000000000000000000..70f7b1f4899159939d6112a9d15f42625b9574dd --- /dev/null +++ b/upcoming-release-notes/2771.md @@ -0,0 +1,6 @@ +--- +category: Maintenance +authors: [MatissJanis] +--- + +Migrating native `Tooltip` component to react-aria Tooltip/Popover (vol.6)