diff --git a/packages/desktop-client/src/components/LoggedInUser.tsx b/packages/desktop-client/src/components/LoggedInUser.tsx index f1043b764db27d5aa12b18709a9a067d4b6c19dc..c9eba0badb8517bd6f02ca2323debb1241aac2c3 100644 --- a/packages/desktop-client/src/components/LoggedInUser.tsx +++ b/packages/desktop-client/src/components/LoggedInUser.tsx @@ -1,5 +1,5 @@ // @ts-strict-ignore -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useRef } from 'react'; import { useSelector } from 'react-redux'; import { type State } from 'loot-core/src/client/state-types'; @@ -9,10 +9,10 @@ import { theme, styles, type CSSProperties } 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 { useServerURL } from './ServerContext'; -import { Tooltip } from './tooltips'; type LoggedInUserProps = { hideIfNoServer?: boolean; @@ -29,6 +29,7 @@ export function LoggedInUser({ const [loading, setLoading] = useState(true); const [menuOpen, setMenuOpen] = useState(false); const serverUrl = useServerURL(); + const triggerRef = useRef(null); useEffect(() => { getUserData().then(() => setLoading(false)); @@ -95,6 +96,7 @@ export function LoggedInUser({ return ( <View style={{ flexDirection: 'row', alignItems: 'center', ...style }}> <Button + ref={triggerRef} type="bare" onClick={() => setMenuOpen(true)} style={color && { color }} @@ -102,29 +104,27 @@ export function LoggedInUser({ {serverMessage()} </Button> - {menuOpen && ( - <Tooltip - position="bottom-right" - style={{ padding: 0 }} - onClose={() => setMenuOpen(false)} - > - <Menu - onMenuSelect={onMenuSelect} - items={[ - serverUrl && - !userData?.offline && { - name: 'change-password', - text: 'Change password', - }, - serverUrl && { name: 'sign-out', text: 'Sign out' }, - { - name: 'config-server', - text: serverUrl ? 'Change server URL' : 'Start using a server', + <Popover + triggerRef={triggerRef} + isOpen={menuOpen} + onOpenChange={() => setMenuOpen(false)} + > + <Menu + onMenuSelect={onMenuSelect} + items={[ + serverUrl && + !userData?.offline && { + name: 'change-password', + text: 'Change password', }, - ]} - /> - </Tooltip> - )} + serverUrl && { name: 'sign-out', text: 'Sign out' }, + { + name: 'config-server', + text: serverUrl ? 'Change server URL' : 'Start using a server', + }, + ]} + /> + </Popover> </View> ); } diff --git a/packages/desktop-client/src/components/NotesButton.tsx b/packages/desktop-client/src/components/NotesButton.tsx index 31868f52120c1f912c0f57a38b050cfacad5df28..f39af29506a5b17fe773223318b01f1ee188bcf7 100644 --- a/packages/desktop-client/src/components/NotesButton.tsx +++ b/packages/desktop-client/src/components/NotesButton.tsx @@ -1,5 +1,4 @@ import React, { useEffect, useRef, useState, type ComponentProps } from 'react'; -import { Popover } from 'react-aria-components'; import { useLiveQuery } from 'loot-core/src/client/query-hooks'; import { send } from 'loot-core/src/platform/client/fetch'; @@ -7,9 +6,10 @@ import { q } from 'loot-core/src/shared/query'; import { type NoteEntity } from 'loot-core/types/models'; import { SvgCustomNotesPaper } from '../icons/v2'; -import { type CSSProperties, styles, theme } from '../style'; +import { type CSSProperties, theme } from '../style'; import { Button } from './common/Button'; +import { Popover } from './common/Popover'; import { Tooltip } from './common/Tooltip'; import { View } from './common/View'; import { Notes } from './Notes'; @@ -81,7 +81,7 @@ export function NotesButton({ isOpen={isOpen} onOpenChange={onClose} placement={tooltipPosition} - style={{ ...styles.tooltip, marginTop: -8 }} + style={{ padding: 4 }} > <Notes notes={tempNotes} editable focused onChange={setTempNotes} /> </Popover> diff --git a/packages/desktop-client/src/components/ThemeSelector.tsx b/packages/desktop-client/src/components/ThemeSelector.tsx index ed2a49508a213fbefcbcb1debcae32986e9589bc..1127ec128bc0b1171f305c70801c1c546c28a918 100644 --- a/packages/desktop-client/src/components/ThemeSelector.tsx +++ b/packages/desktop-client/src/components/ThemeSelector.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useRef, useState } from 'react'; import type { Theme } from 'loot-core/src/types/prefs'; @@ -8,7 +8,7 @@ import { type CSSProperties, themeOptions, useTheme } from '../style'; import { Button } from './common/Button'; import { Menu } from './common/Menu'; -import { Tooltip } from './tooltips'; +import { Popover } from './common/Popover'; type ThemeSelectorProps = { style?: CSSProperties; @@ -17,6 +17,7 @@ type ThemeSelectorProps = { export function ThemeSelector({ style }: ThemeSelectorProps) { const [theme, switchTheme] = useTheme(); const [menuOpen, setMenuOpen] = useState(false); + const triggerRef = useRef(null); const { isNarrowWidth } = useResponsive(); @@ -34,26 +35,32 @@ export function ThemeSelector({ style }: ThemeSelectorProps) { const Icon = themeIcons[theme] || SvgSun; - return isNarrowWidth ? null : ( - <Button - type="bare" - aria-label="Switch theme" - onClick={() => setMenuOpen(true)} - style={style} - > - <Icon style={{ width: 13, height: 13, color: 'inherit' }} /> - {menuOpen && ( - <Tooltip - position="bottom-right" - style={{ padding: 0 }} - onClose={() => setMenuOpen(false)} - > - <Menu - onMenuSelect={onMenuSelect} - items={themeOptions.map(([name, text]) => ({ name, text }))} - /> - </Tooltip> - )} - </Button> + if (isNarrowWidth) { + return null; + } + + return ( + <> + <Button + ref={triggerRef} + type="bare" + aria-label="Switch theme" + onClick={() => setMenuOpen(true)} + style={style} + > + <Icon style={{ width: 13, height: 13, color: 'inherit' }} /> + </Button> + + <Popover + triggerRef={triggerRef} + isOpen={menuOpen} + onOpenChange={() => setMenuOpen(false)} + > + <Menu + onMenuSelect={onMenuSelect} + items={themeOptions.map(([name, text]) => ({ name, text }))} + /> + </Popover> + </> ); } diff --git a/packages/desktop-client/src/components/Titlebar.tsx b/packages/desktop-client/src/components/Titlebar.tsx index 1d91cbc247e6f93e3b8559c2694c32a8122a4a7a..10d4ead37151ecba7ac68508c752cc43f051642e 100644 --- a/packages/desktop-client/src/components/Titlebar.tsx +++ b/packages/desktop-client/src/components/Titlebar.tsx @@ -35,6 +35,7 @@ import { MonthCountSelector } from './budget/MonthCountSelector'; import { Button, ButtonWithLoading } from './common/Button'; import { Link } from './common/Link'; import { Paragraph } from './common/Paragraph'; +import { Popover } from './common/Popover'; import { Text } from './common/Text'; import { View } from './common/View'; import { LoggedInUser } from './LoggedInUser'; @@ -42,7 +43,6 @@ import { useServerURL } from './ServerContext'; import { useSidebar } from './sidebar/SidebarProvider'; import { useSheetValue } from './spreadsheet/useSheetValue'; import { ThemeSelector } from './ThemeSelector'; -import { Tooltip } from './tooltips'; export const SWITCH_BUDGET_MESSAGE_TYPE = 'budget/switch-type'; @@ -290,7 +290,8 @@ function BudgetTitlebar() { const { sendEvent } = useContext(TitlebarContext); const [loading, setLoading] = useState(false); - const [showTooltip, setShowTooltip] = useState(false); + const [showPopover, setShowPopover] = useState(false); + const triggerRef = useRef(null); const reportBudgetEnabled = useFeatureFlag('reportBudget'); @@ -320,6 +321,7 @@ function BudgetTitlebar() { {reportBudgetEnabled && ( <View style={{ marginLeft: -5 }}> <ButtonWithLoading + ref={triggerRef} type="bare" loading={loading} style={{ @@ -327,52 +329,48 @@ function BudgetTitlebar() { padding: '4px 7px', }} title="Learn more about budgeting" - onClick={() => setShowTooltip(true)} + onClick={() => setShowPopover(true)} > {budgetType === 'report' ? 'Report budget' : 'Rollover budget'} </ButtonWithLoading> - {showTooltip && ( - <Tooltip - position="bottom-left" - onClose={() => setShowTooltip(false)} - style={{ - padding: 10, - maxWidth: 400, - }} - > - <Paragraph> - You are currently using a{' '} - <Text style={{ fontWeight: 600 }}> - {budgetType === 'report' - ? 'Report budget' - : 'Rollover budget'} - . - </Text>{' '} - Switching will not lose any data and you can always switch back. - </Paragraph> - <Paragraph> - <ButtonWithLoading - type="primary" - loading={loading} - onClick={onSwitchType} - > - Switch to a{' '} - {budgetType === 'report' - ? 'Rollover budget' - : 'Report budget'} - </ButtonWithLoading> - </Paragraph> - <Paragraph isLast={true}> - <Link - variant="external" - to="https://actualbudget.org/docs/experimental/report-budget" - linkColor="muted" - > - How do these types of budgeting work? - </Link> - </Paragraph> - </Tooltip> - )} + + <Popover + triggerRef={triggerRef} + placement="bottom start" + isOpen={showPopover} + onOpenChange={() => setShowPopover(false)} + style={{ + padding: 10, + maxWidth: 400, + }} + > + <Paragraph> + You are currently using a{' '} + <Text style={{ fontWeight: 600 }}> + {budgetType === 'report' ? 'Report budget' : 'Rollover budget'}. + </Text>{' '} + Switching will not lose any data and you can always switch back. + </Paragraph> + <Paragraph> + <ButtonWithLoading + type="primary" + loading={loading} + onClick={onSwitchType} + > + Switch to a{' '} + {budgetType === 'report' ? 'Rollover budget' : 'Report budget'} + </ButtonWithLoading> + </Paragraph> + <Paragraph isLast={true}> + <Link + variant="external" + to="https://actualbudget.org/docs/experimental/report-budget" + linkColor="muted" + > + How do these types of budgeting work? + </Link> + </Paragraph> + </Popover> </View> )} </View> diff --git a/packages/desktop-client/src/components/accounts/AccountSyncCheck.jsx b/packages/desktop-client/src/components/accounts/AccountSyncCheck.jsx index f253a51f162639249af21837e4dea5a882fd6537..6778992340c2f29829f60dc02dab38a10fa3bedd 100644 --- a/packages/desktop-client/src/components/accounts/AccountSyncCheck.jsx +++ b/packages/desktop-client/src/components/accounts/AccountSyncCheck.jsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useRef, useState } from 'react'; import { useSelector } from 'react-redux'; import { useParams } from 'react-router-dom'; @@ -9,8 +9,8 @@ import { SvgExclamationOutline } from '../../icons/v1'; import { theme } from '../../style'; import { Button } from '../common/Button'; import { Link } from '../common/Link'; +import { Popover } from '../common/Popover'; import { View } from '../common/View'; -import { Tooltip } from '../tooltips'; function getErrorMessage(type, code) { switch (type.toUpperCase()) { @@ -56,6 +56,8 @@ export function AccountSyncCheck() { const { id } = useParams(); const [open, setOpen] = useState(false); + const triggerRef = useRef(null); + if (!failedAccounts) { return null; } @@ -85,6 +87,7 @@ export function AccountSyncCheck() { return ( <View> <Button + ref={triggerRef} type="bare" style={{ flexDirection: 'row', @@ -102,38 +105,34 @@ export function AccountSyncCheck() { This account is experiencing connection problems. Let’s fix it. </Button> - {open && ( - <Tooltip - position="bottom-left" - onClose={() => setOpen(false)} - style={{ fontSize: 14, padding: 15, maxWidth: 400 }} - > - <div style={{ marginBottom: '1.15em' }}> - The server returned the following error: - </div> - - <div style={{ marginBottom: '1.25em', color: theme.errorText }}> - {getErrorMessage(error.type, error.code)} - </div> - - <View style={{ justifyContent: 'flex-end', flexDirection: 'row' }}> - {showAuth ? ( - <> - <Button onClick={unlink}>Unlink</Button> - <Button - type="primary" - onClick={reauth} - style={{ marginLeft: 5 }} - > - Reauthorize - </Button> - </> - ) : ( - <Button onClick={unlink}>Unlink account</Button> - )} - </View> - </Tooltip> - )} + <Popover + triggerRef={triggerRef} + placement="bottom start" + isOpen={open} + onOpenChange={() => setOpen(false)} + style={{ fontSize: 14, padding: 15, maxWidth: 400 }} + > + <div style={{ marginBottom: '1.15em' }}> + The server returned the following error: + </div> + + <div style={{ marginBottom: '1.25em', color: theme.errorText }}> + {getErrorMessage(error.type, error.code)} + </div> + + <View style={{ justifyContent: 'flex-end', flexDirection: 'row' }}> + {showAuth ? ( + <> + <Button onClick={unlink}>Unlink</Button> + <Button type="primary" onClick={reauth} style={{ marginLeft: 5 }}> + Reauthorize + </Button> + </> + ) : ( + <Button onClick={unlink}>Unlink account</Button> + )} + </View> + </Popover> </View> ); } diff --git a/packages/desktop-client/src/components/common/Popover.tsx b/packages/desktop-client/src/components/common/Popover.tsx new file mode 100644 index 0000000000000000000000000000000000000000..94b029f74510de7bc317bffbbfd045d75cd7265a --- /dev/null +++ b/packages/desktop-client/src/components/common/Popover.tsx @@ -0,0 +1,17 @@ +import { type ComponentProps } from 'react'; +import { Popover as ReactAriaPopover } from 'react-aria-components'; + +import { styles } from '../../style'; + +type PopoverProps = ComponentProps<typeof ReactAriaPopover>; + +export const Popover = ({ style = {}, ...props }: PopoverProps) => { + return ( + <ReactAriaPopover + placement="bottom end" + offset={0} + style={{ ...styles.tooltip, padding: 0, ...style }} + {...props} + /> + ); +}; diff --git a/packages/desktop-client/src/components/sidebar/Sidebar.tsx b/packages/desktop-client/src/components/sidebar/Sidebar.tsx index bd6bc5335f4d80ea0e99e0028d5be731af39d817..77e863ccf18d3f9166ab82a6b37a536299b059d6 100644 --- a/packages/desktop-client/src/components/sidebar/Sidebar.tsx +++ b/packages/desktop-client/src/components/sidebar/Sidebar.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useRef, useState } from 'react'; import { useDispatch } from 'react-redux'; import { @@ -20,9 +20,9 @@ import { Button } from '../common/Button'; import { InitialFocus } from '../common/InitialFocus'; 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 { Tooltip } from '../tooltips'; import { Accounts } from './Accounts'; import { Item } from './Item'; @@ -145,6 +145,7 @@ function EditableBudgetName() { const [budgetName, setBudgetNamePref] = useLocalPref('budgetName'); const [editing, setEditing] = useState(false); const [menuOpen, setMenuOpen] = useState(false); + const triggerRef = useRef(null); function onMenuSelect(type: string) { setMenuOpen(false); @@ -195,9 +196,12 @@ function EditableBudgetName() { /> </InitialFocus> ); - } else { - return ( + } + + return ( + <> <Button + ref={triggerRef} type="bare" color={theme.buttonNormalBorder} style={{ @@ -212,16 +216,16 @@ function EditableBudgetName() { {budgetName || 'A budget has no name'} </Text> <SvgExpandArrow width={7} height={7} style={{ marginLeft: 5 }} /> - {menuOpen && ( - <Tooltip - position="bottom-left" - style={{ padding: 0 }} - onClose={() => setMenuOpen(false)} - > - <Menu onMenuSelect={onMenuSelect} items={items} /> - </Tooltip> - )} </Button> - ); - } + + <Popover + triggerRef={triggerRef} + placement="bottom start" + isOpen={menuOpen} + onOpenChange={() => setMenuOpen(false)} + > + <Menu onMenuSelect={onMenuSelect} items={items} /> + </Popover> + </> + ); } diff --git a/upcoming-release-notes/2593.md b/upcoming-release-notes/2593.md new file mode 100644 index 0000000000000000000000000000000000000000..b94f89dfbf7db621be57b902bd55314e4633cce4 --- /dev/null +++ b/upcoming-release-notes/2593.md @@ -0,0 +1,6 @@ +--- +category: Maintenance +authors: [MatissJanis] +--- + +Migrating native `Tooltip` component to react-aria Tooltip/Popover (vol.2)