mirror of
https://github.com/actualbudget/actual.git
synced 2026-03-21 15:36:50 -05:00
🐛 fix menu not closing when menu item is clicked (#4716)
This commit is contained in:
committed by
GitHub
parent
e04ca554e2
commit
aef38f1679
@@ -3,11 +3,13 @@ import {
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
type ComponentProps,
|
||||
type ComponentType,
|
||||
type SVGProps,
|
||||
type CSSProperties,
|
||||
} from 'react';
|
||||
|
||||
import { Button } from './Button';
|
||||
import { Text } from './Text';
|
||||
import { theme } from './theme';
|
||||
import { Toggle } from './Toggle';
|
||||
@@ -61,6 +63,7 @@ type MenuProps<NameType> = {
|
||||
style?: CSSProperties;
|
||||
className?: string;
|
||||
getItemStyle?: (item: MenuItemObject<NameType>) => CSSProperties;
|
||||
slot?: ComponentProps<typeof Button>['slot'];
|
||||
};
|
||||
|
||||
export function Menu<const NameType = string>({
|
||||
@@ -71,6 +74,7 @@ export function Menu<const NameType = string>({
|
||||
style,
|
||||
className,
|
||||
getItemStyle,
|
||||
slot,
|
||||
}: MenuProps<NameType>) {
|
||||
const elRef = useRef<HTMLDivElement>(null);
|
||||
const items = allItems.filter(x => x);
|
||||
@@ -161,9 +165,10 @@ export function Menu<const NameType = string>({
|
||||
const Icon = item.icon;
|
||||
|
||||
return (
|
||||
<View
|
||||
role="button"
|
||||
<Button
|
||||
key={String(item.name)}
|
||||
variant="bare"
|
||||
slot={slot}
|
||||
style={{
|
||||
cursor: 'default',
|
||||
padding: 10,
|
||||
@@ -179,11 +184,9 @@ export function Menu<const NameType = string>({
|
||||
}),
|
||||
...(!isLabel(item) && getItemStyle?.(item)),
|
||||
}}
|
||||
onPointerEnter={() => setHoveredIndex(idx)}
|
||||
onPointerLeave={() => setHoveredIndex(null)}
|
||||
onPointerUp={e => {
|
||||
e.stopPropagation();
|
||||
|
||||
onHoverStart={() => setHoveredIndex(idx)}
|
||||
onHoverEnd={() => setHoveredIndex(null)}
|
||||
onPress={() => {
|
||||
if (
|
||||
!item.disabled &&
|
||||
item.toggle === undefined &&
|
||||
@@ -232,7 +235,7 @@ export function Menu<const NameType = string>({
|
||||
</View>
|
||||
)}
|
||||
{item.key && <Keybinding keyName={item.key} />}
|
||||
</View>
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
{footer}
|
||||
|
||||
@@ -180,7 +180,11 @@ export class MobileBudgetPage {
|
||||
const categoryButton = await this.#getButtonForCategory(categoryName);
|
||||
await categoryButton.click();
|
||||
|
||||
return new CategoryMenuModal(this.page.getByRole('dialog'));
|
||||
return new CategoryMenuModal(
|
||||
this.page.getByRole('dialog', {
|
||||
name: 'Modal dialog',
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async #getButtonForCell(
|
||||
@@ -222,7 +226,11 @@ export class MobileBudgetPage {
|
||||
const budgetedButton = await this.getButtonForBudgeted(categoryName);
|
||||
await budgetedButton.click();
|
||||
|
||||
return new BudgetMenuModal(this.page.getByRole('dialog'));
|
||||
return new BudgetMenuModal(
|
||||
this.page.getByRole('dialog', {
|
||||
name: 'Modal dialog',
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async openSpentPage(categoryName: string) {
|
||||
@@ -239,7 +247,11 @@ export class MobileBudgetPage {
|
||||
|
||||
if (await balanceButton.isVisible()) {
|
||||
await balanceButton.click();
|
||||
return new BalanceMenuModal(this.page.getByRole('dialog'));
|
||||
return new BalanceMenuModal(
|
||||
this.page.getByRole('dialog', {
|
||||
name: 'Modal dialog',
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
throw new Error(
|
||||
`Balance button for category ${categoryName} not found or not visible.`,
|
||||
@@ -324,7 +336,11 @@ export class MobileBudgetPage {
|
||||
}
|
||||
await budgetSummaryButton.click();
|
||||
|
||||
return new EnvelopeBudgetSummaryModal(this.page.getByRole('dialog'));
|
||||
return new EnvelopeBudgetSummaryModal(
|
||||
this.page.getByRole('dialog', {
|
||||
name: 'Modal dialog',
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async #getButtonForTrackingBudgetSummary({
|
||||
@@ -358,6 +374,10 @@ export class MobileBudgetPage {
|
||||
}
|
||||
await budgetSummaryButton.click();
|
||||
|
||||
return new TrackingBudgetSummaryModal(this.page.getByRole('dialog'));
|
||||
return new TrackingBudgetSummaryModal(
|
||||
this.page.getByRole('dialog', {
|
||||
name: 'Modal dialog',
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,10 @@ export class CategoryMenuModal {
|
||||
async editNotes() {
|
||||
await this.editNotesButton.click();
|
||||
|
||||
return new EditNotesModal(this.page.getByRole('dialog'));
|
||||
return new EditNotesModal(
|
||||
this.page.getByRole('dialog', {
|
||||
name: 'Modal dialog',
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import React, {
|
||||
type ReactNode,
|
||||
type ComponentProps,
|
||||
} from 'react';
|
||||
import { DialogTrigger } from 'react-aria-components';
|
||||
import { Dialog, DialogTrigger } from 'react-aria-components';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
|
||||
@@ -463,18 +463,20 @@ export function AccountHeader({
|
||||
</Button>
|
||||
|
||||
<Popover style={{ width: 275 }}>
|
||||
<AccountMenu
|
||||
account={account}
|
||||
canSync={canSync}
|
||||
canShowBalances={
|
||||
canCalculateBalance ? canCalculateBalance() : false
|
||||
}
|
||||
isSorted={isSorted}
|
||||
showBalances={showBalances}
|
||||
showCleared={showCleared}
|
||||
showReconciled={showReconciled}
|
||||
onMenuSelect={onMenuSelect}
|
||||
/>
|
||||
<Dialog>
|
||||
<AccountMenu
|
||||
account={account}
|
||||
canSync={canSync}
|
||||
canShowBalances={
|
||||
canCalculateBalance ? canCalculateBalance() : false
|
||||
}
|
||||
isSorted={isSorted}
|
||||
showBalances={showBalances}
|
||||
showCleared={showCleared}
|
||||
showReconciled={showReconciled}
|
||||
onMenuSelect={onMenuSelect}
|
||||
/>
|
||||
</Dialog>
|
||||
</Popover>
|
||||
</DialogTrigger>
|
||||
</View>
|
||||
@@ -490,20 +492,23 @@ export function AccountHeader({
|
||||
</Button>
|
||||
|
||||
<Popover>
|
||||
<Menu
|
||||
onMenuSelect={onMenuSelect}
|
||||
items={[
|
||||
...(isSorted
|
||||
? [
|
||||
{
|
||||
name: 'remove-sorting',
|
||||
text: t('Remove all sorting'),
|
||||
} as const,
|
||||
]
|
||||
: []),
|
||||
{ name: 'export', text: t('Export') },
|
||||
]}
|
||||
/>
|
||||
<Dialog>
|
||||
<Menu
|
||||
slot="close"
|
||||
onMenuSelect={onMenuSelect}
|
||||
items={[
|
||||
...(isSorted
|
||||
? [
|
||||
{
|
||||
name: 'remove-sorting',
|
||||
text: t('Remove all sorting'),
|
||||
} as const,
|
||||
]
|
||||
: []),
|
||||
{ name: 'export', text: t('Export') },
|
||||
]}
|
||||
/>
|
||||
</Dialog>
|
||||
</Popover>
|
||||
</DialogTrigger>
|
||||
</View>
|
||||
@@ -722,6 +727,7 @@ function AccountMenu({
|
||||
|
||||
return (
|
||||
<Menu
|
||||
slot="close"
|
||||
onMenuSelect={item => {
|
||||
onMenuSelect(item);
|
||||
}}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { DialogTrigger } from 'react-aria-components';
|
||||
import { Dialog, DialogTrigger } from 'react-aria-components';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
|
||||
import { Button, ButtonWithLoading } from '@actual-app/components/button';
|
||||
@@ -416,19 +416,21 @@ export function CreateAccountModal({
|
||||
</Button>
|
||||
|
||||
<Popover>
|
||||
<Menu
|
||||
onMenuSelect={item => {
|
||||
if (item === 'reconfigure') {
|
||||
onGoCardlessReset();
|
||||
}
|
||||
}}
|
||||
items={[
|
||||
{
|
||||
name: 'reconfigure',
|
||||
text: t('Reset GoCardless credentials'),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Dialog>
|
||||
<Menu
|
||||
onMenuSelect={item => {
|
||||
if (item === 'reconfigure') {
|
||||
onGoCardlessReset();
|
||||
}
|
||||
}}
|
||||
items={[
|
||||
{
|
||||
name: 'reconfigure',
|
||||
text: t('Reset GoCardless credentials'),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Dialog>
|
||||
</Popover>
|
||||
</DialogTrigger>
|
||||
)}
|
||||
@@ -479,19 +481,21 @@ export function CreateAccountModal({
|
||||
/>
|
||||
</Button>
|
||||
<Popover>
|
||||
<Menu
|
||||
onMenuSelect={item => {
|
||||
if (item === 'reconfigure') {
|
||||
onSimpleFinReset();
|
||||
}
|
||||
}}
|
||||
items={[
|
||||
{
|
||||
name: 'reconfigure',
|
||||
text: t('Reset SimpleFIN credentials'),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Dialog>
|
||||
<Menu
|
||||
onMenuSelect={item => {
|
||||
if (item === 'reconfigure') {
|
||||
onSimpleFinReset();
|
||||
}
|
||||
}}
|
||||
items={[
|
||||
{
|
||||
name: 'reconfigure',
|
||||
text: t('Reset SimpleFIN credentials'),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Dialog>
|
||||
</Popover>
|
||||
</DialogTrigger>
|
||||
)}
|
||||
@@ -543,19 +547,23 @@ export function CreateAccountModal({
|
||||
</Button>
|
||||
|
||||
<Popover>
|
||||
<Menu
|
||||
onMenuSelect={item => {
|
||||
if (item === 'reconfigure') {
|
||||
onPluggyAiReset();
|
||||
}
|
||||
}}
|
||||
items={[
|
||||
{
|
||||
name: 'reconfigure',
|
||||
text: t('Reset Pluggy.ai credentials'),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Dialog>
|
||||
<Menu
|
||||
onMenuSelect={item => {
|
||||
if (item === 'reconfigure') {
|
||||
onPluggyAiReset();
|
||||
}
|
||||
}}
|
||||
items={[
|
||||
{
|
||||
name: 'reconfigure',
|
||||
text: t(
|
||||
'Reset Pluggy.ai credentials',
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Dialog>
|
||||
</Popover>
|
||||
</DialogTrigger>
|
||||
)}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { DialogTrigger } from 'react-aria-components';
|
||||
import { Dialog, DialogTrigger } from 'react-aria-components';
|
||||
import { Responsive, WidthProvider, type Layout } from 'react-grid-layout';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
@@ -339,73 +339,76 @@ export function Overview() {
|
||||
</Button>
|
||||
|
||||
<Popover>
|
||||
<Menu
|
||||
onMenuSelect={item => {
|
||||
if (item === 'custom-report') {
|
||||
navigate('/reports/custom');
|
||||
return;
|
||||
}
|
||||
<Dialog>
|
||||
<Menu
|
||||
slot="close"
|
||||
onMenuSelect={item => {
|
||||
if (item === 'custom-report') {
|
||||
navigate('/reports/custom');
|
||||
return;
|
||||
}
|
||||
|
||||
function isExistingCustomReport(
|
||||
name: string,
|
||||
): name is `custom-report-${string}` {
|
||||
return name.startsWith('custom-report-');
|
||||
}
|
||||
if (isExistingCustomReport(item)) {
|
||||
const [, reportId] = item.split('custom-report-');
|
||||
onAddWidget<CustomReportWidget>('custom-report', {
|
||||
id: reportId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
function isExistingCustomReport(
|
||||
name: string,
|
||||
): name is `custom-report-${string}` {
|
||||
return name.startsWith('custom-report-');
|
||||
}
|
||||
if (isExistingCustomReport(item)) {
|
||||
const [, reportId] = item.split('custom-report-');
|
||||
onAddWidget<CustomReportWidget>('custom-report', {
|
||||
id: reportId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (item === 'markdown-card') {
|
||||
onAddWidget<MarkdownWidget>(item, {
|
||||
content: `### ${t('Text Widget')}\n\n${t('Edit this widget to change the **markdown** content.')}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (item === 'markdown-card') {
|
||||
onAddWidget<MarkdownWidget>(item, {
|
||||
content: `### ${t('Text Widget')}\n\n${t('Edit this widget to change the **markdown** content.')}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
onAddWidget(item);
|
||||
}}
|
||||
items={[
|
||||
{
|
||||
name: 'cash-flow-card' as const,
|
||||
text: t('Cash flow graph'),
|
||||
},
|
||||
{
|
||||
name: 'net-worth-card' as const,
|
||||
text: t('Net worth graph'),
|
||||
},
|
||||
{
|
||||
name: 'spending-card' as const,
|
||||
text: t('Spending analysis'),
|
||||
},
|
||||
{
|
||||
name: 'markdown-card' as const,
|
||||
text: t('Text widget'),
|
||||
},
|
||||
{
|
||||
name: 'summary-card' as const,
|
||||
text: t('Summary card'),
|
||||
},
|
||||
{
|
||||
name: 'calendar-card' as const,
|
||||
text: t('Calendar card'),
|
||||
},
|
||||
{
|
||||
name: 'custom-report' as const,
|
||||
text: t('New custom report'),
|
||||
},
|
||||
...(customReports.length
|
||||
? ([Menu.line] satisfies Array<typeof Menu.line>)
|
||||
: []),
|
||||
...customReports.map(report => ({
|
||||
name: `custom-report-${report.id}` as const,
|
||||
text: report.name,
|
||||
})),
|
||||
]}
|
||||
/>
|
||||
onAddWidget(item);
|
||||
}}
|
||||
items={[
|
||||
{
|
||||
name: 'cash-flow-card' as const,
|
||||
text: t('Cash flow graph'),
|
||||
},
|
||||
{
|
||||
name: 'net-worth-card' as const,
|
||||
text: t('Net worth graph'),
|
||||
},
|
||||
{
|
||||
name: 'spending-card' as const,
|
||||
text: t('Spending analysis'),
|
||||
},
|
||||
{
|
||||
name: 'markdown-card' as const,
|
||||
text: t('Text widget'),
|
||||
},
|
||||
{
|
||||
name: 'summary-card' as const,
|
||||
text: t('Summary card'),
|
||||
},
|
||||
{
|
||||
name: 'calendar-card' as const,
|
||||
text: t('Calendar card'),
|
||||
},
|
||||
{
|
||||
name: 'custom-report' as const,
|
||||
text: t('New custom report'),
|
||||
},
|
||||
...(customReports.length
|
||||
? ([Menu.line] satisfies Array<typeof Menu.line>)
|
||||
: []),
|
||||
...customReports.map(report => ({
|
||||
name: `custom-report-${report.id}` as const,
|
||||
text: report.name,
|
||||
})),
|
||||
]}
|
||||
/>
|
||||
</Dialog>
|
||||
</Popover>
|
||||
</DialogTrigger>
|
||||
|
||||
@@ -434,39 +437,42 @@ export function Overview() {
|
||||
/>
|
||||
</Button>
|
||||
<Popover>
|
||||
<Menu
|
||||
onMenuSelect={item => {
|
||||
switch (item) {
|
||||
case 'reset':
|
||||
onResetDashboard();
|
||||
break;
|
||||
case 'export':
|
||||
onExport();
|
||||
break;
|
||||
case 'import':
|
||||
onImport();
|
||||
break;
|
||||
}
|
||||
}}
|
||||
items={[
|
||||
{
|
||||
name: 'reset',
|
||||
text: t('Reset to default'),
|
||||
disabled: isImporting,
|
||||
},
|
||||
Menu.line,
|
||||
{
|
||||
name: 'import',
|
||||
text: t('Import'),
|
||||
disabled: isImporting,
|
||||
},
|
||||
{
|
||||
name: 'export',
|
||||
text: t('Export'),
|
||||
disabled: isImporting,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Dialog>
|
||||
<Menu
|
||||
slot="close"
|
||||
onMenuSelect={item => {
|
||||
switch (item) {
|
||||
case 'reset':
|
||||
onResetDashboard();
|
||||
break;
|
||||
case 'export':
|
||||
onExport();
|
||||
break;
|
||||
case 'import':
|
||||
onImport();
|
||||
break;
|
||||
}
|
||||
}}
|
||||
items={[
|
||||
{
|
||||
name: 'reset',
|
||||
text: t('Reset to default'),
|
||||
disabled: isImporting,
|
||||
},
|
||||
Menu.line,
|
||||
{
|
||||
name: 'import',
|
||||
text: t('Import'),
|
||||
disabled: isImporting,
|
||||
},
|
||||
{
|
||||
name: 'export',
|
||||
text: t('Export'),
|
||||
disabled: isImporting,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Dialog>
|
||||
</Popover>
|
||||
</DialogTrigger>
|
||||
</>
|
||||
|
||||
6
upcoming-release-notes/4716.md
Normal file
6
upcoming-release-notes/4716.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
category: Bugfix
|
||||
authors: [MatissJanis]
|
||||
---
|
||||
|
||||
Fix menu not closing when menu item is clicked
|
||||
Reference in New Issue
Block a user