mirror of
https://github.com/actualbudget/actual.git
synced 2026-03-11 20:44:32 -05:00
♻️ (tooltip) refactoring to react-aria (vol.9) (#2826)
This commit is contained in:
committed by
GitHub
parent
abc4636662
commit
64cd6ee3c9
@@ -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)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
6
upcoming-release-notes/2826.md
Normal file
6
upcoming-release-notes/2826.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
category: Maintenance
|
||||
authors: [MatissJanis]
|
||||
---
|
||||
|
||||
Migrating native `Tooltip` component to react-aria Tooltip/Popover (vol.9)
|
||||
Reference in New Issue
Block a user