Skip to content

Commit 4f890f4

Browse files
committed
fix(web): keep event action menu open
1 parent 2352bc0 commit 4f890f4

4 files changed

Lines changed: 101 additions & 3 deletions

File tree

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { fireEvent, render, screen } from "@testing-library/react";
2+
import { type PropsWithChildren, type ReactElement } from "react";
3+
import { ThemeProvider } from "styled-components";
4+
import { theme } from "@web/common/styles/theme";
5+
import { afterEach, describe, expect, it, mock } from "bun:test";
6+
7+
let listNavigationProps: { focusItemOnHover?: boolean } | null = null;
8+
let floatingOptions: { onOpenChange?: (open: boolean) => void } | null = null;
9+
let focusManagerProps: { closeOnFocusOut?: boolean } | null = null;
10+
11+
mock.module("@floating-ui/react", () => ({
12+
autoUpdate: mock(),
13+
flip: mock(() => ({})),
14+
FloatingFocusManager: ({
15+
children,
16+
...props
17+
}: PropsWithChildren<{ closeOnFocusOut?: boolean }>) => {
18+
focusManagerProps = props;
19+
return <>{children}</>;
20+
},
21+
FloatingPortal: ({ children }: PropsWithChildren) => <>{children}</>,
22+
offset: mock(() => ({})),
23+
shift: mock(() => ({})),
24+
useClick: mock(() => ({})),
25+
useDismiss: mock(() => ({})),
26+
useFloating: (options: { onOpenChange?: (open: boolean) => void }) => {
27+
floatingOptions = options;
28+
return {
29+
context: {
30+
floatingStyles: {},
31+
},
32+
refs: {
33+
setFloating: mock(),
34+
setReference: mock(),
35+
},
36+
};
37+
},
38+
useInteractions: mock(() => ({
39+
getFloatingProps: (props = {}) => props,
40+
getItemProps: (props = {}) => props,
41+
getReferenceProps: (
42+
props: { onClick?: (event: MouseEvent) => void } = {},
43+
) => ({
44+
...props,
45+
onClick: (event: MouseEvent) => {
46+
props.onClick?.(event);
47+
floatingOptions?.onOpenChange?.(true);
48+
},
49+
}),
50+
})),
51+
useListNavigation: mock((_context, props) => {
52+
listNavigationProps = props as { focusItemOnHover?: boolean };
53+
return {};
54+
}),
55+
useRole: mock(() => ({})),
56+
}));
57+
58+
mock.module("@web/common/hooks/useGridMaxZIndex", () => ({
59+
useGridMaxZIndex: () => 0,
60+
}));
61+
62+
const { ActionsMenu } =
63+
require("./ActionsMenu") as typeof import("./ActionsMenu");
64+
65+
const renderWithTheme = (ui: ReactElement) =>
66+
render(<ThemeProvider theme={theme}>{ui}</ThemeProvider>);
67+
68+
describe("ActionsMenu", () => {
69+
afterEach(() => {
70+
floatingOptions = null;
71+
focusManagerProps = null;
72+
listNavigationProps = null;
73+
});
74+
75+
it("keeps hover from moving focus to menu items", () => {
76+
renderWithTheme(<ActionsMenu bgColor="#fff">{() => null}</ActionsMenu>);
77+
78+
expect(listNavigationProps?.focusItemOnHover).toBe(false);
79+
});
80+
81+
it("keeps the floating menu from dismissing on focus movement", () => {
82+
renderWithTheme(<ActionsMenu bgColor="#fff">{() => null}</ActionsMenu>);
83+
84+
fireEvent.click(screen.getByRole("button", { name: "Open actions menu" }));
85+
86+
expect(focusManagerProps?.closeOnFocusOut).toBe(false);
87+
});
88+
});

packages/web/src/views/Forms/ActionsMenu/ActionsMenu.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ export const ActionsMenu: React.FC<ActionsMenuProps> = ({
134134
setActiveIndex(sparseIndex);
135135
}
136136
},
137+
focusItemOnHover: false,
137138
loop: true,
138139
});
139140

@@ -179,6 +180,7 @@ export const ActionsMenu: React.FC<ActionsMenuProps> = ({
179180
<FloatingFocusManager
180181
context={context}
181182
modal={false}
183+
closeOnFocusOut={false}
182184
initialFocus={openedByMouseRef.current ? -1 : 0}
183185
returnFocus={false}
184186
>

packages/web/src/views/Week/components/Draft/grid/GridDraft.test.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@ import { DraftContext } from "@web/views/Week/components/Draft/context/DraftCont
1111
import { type WeekProps } from "@web/views/Week/hooks/useWeek";
1212
import { afterEach, describe, expect, it, mock } from "bun:test";
1313

14-
let floatingFocusManagerProps: { modal?: boolean } | null = null;
14+
let floatingFocusManagerProps: {
15+
closeOnFocusOut?: boolean;
16+
modal?: boolean;
17+
} | null = null;
1518

1619
mock.module("@floating-ui/react", () => ({
1720
FloatingFocusManager: ({
@@ -177,10 +180,11 @@ describe("GridDraft keyboard focus", () => {
177180
});
178181
});
179182

180-
it("keeps the floating form non-modal while the draft block is a focus target", () => {
183+
it("keeps the floating form from dismissing when focus moves into nested menus", () => {
181184
renderGridDraft();
182185

183186
expect(floatingFocusManagerProps?.modal).toBe(false);
187+
expect(floatingFocusManagerProps?.closeOnFocusOut).toBe(false);
184188
});
185189

186190
it("keeps an active overlapping saved draft at its stacked width while raising it", () => {

packages/web/src/views/Week/components/Draft/grid/GridDraft.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,11 @@ export const GridDraft: FC<Props> = ({
133133
)}
134134

135135
{isFormOpen && (
136-
<FloatingFocusManager context={context} modal={false}>
136+
<FloatingFocusManager
137+
context={context}
138+
modal={false}
139+
closeOnFocusOut={false}
140+
>
137141
<StyledFloatContainer
138142
ref={refs.setFloating}
139143
strategy={strategy}

0 commit comments

Comments
 (0)