Skip to content

Commit e63b7d8

Browse files
authored
feat(overflow-menu): support e.preventDefault on item click (#2360)
1 parent bdaf6b5 commit e63b7d8

File tree

8 files changed

+132
-27
lines changed

8 files changed

+132
-27
lines changed

COMPONENT_INDEX.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2677,10 +2677,10 @@ None.
26772677

26782678
### Events
26792679

2680-
| Event name | Type | Detail | Description |
2681-
| :--------- | :-------- | :----- | :---------- |
2682-
| click | forwarded | -- | -- |
2683-
| keydown | forwarded | -- | -- |
2680+
| Event name | Type | Detail | Description |
2681+
| :--------- | :--------- | :---------------------- | :---------- |
2682+
| click | dispatched | <code>MouseEvent</code> | -- |
2683+
| keydown | forwarded | -- | -- |
26842684

26852685
## `Pagination`
26862686

docs/src/COMPONENT_API.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10639,9 +10639,9 @@
1063910639
],
1064010640
"events": [
1064110641
{
10642-
"type": "forwarded",
10642+
"type": "dispatched",
1064310643
"name": "click",
10644-
"element": "a"
10644+
"detail": "MouseEvent"
1064510645
},
1064610646
{
1064710647
"type": "forwarded",

docs/src/pages/components/OverflowMenu.svx

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,4 +116,19 @@ Set `disabled` to `true` to disable menu items. Use `hasDivider` to add visual s
116116
text="API documentation"
117117
/>
118118
<OverflowMenuItem hasDivider danger text="Delete service" />
119-
</OverflowMenu>
119+
</OverflowMenu>
120+
121+
## Prevent menu from closing
122+
123+
Use `preventDefault()` on individual `OverflowMenuItem` click events to prevent the menu from closing when that item is clicked. This is useful for scenarios where you want to keep the menu open after performing an action.
124+
125+
<OverflowMenu>
126+
<OverflowMenuItem
127+
text="Show all files"
128+
on:click={(e) => {
129+
// Prevent menu from closing for this item.
130+
e.preventDefault();
131+
}}
132+
/>
133+
<OverflowMenuItem text="Close menu" />
134+
</OverflowMenu>

src/OverflowMenu/OverflowMenu.svelte

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -105,8 +105,14 @@
105105
const update = (id, item) => {
106106
currentId.set(id);
107107
108-
dispatch("close", { index: item.index, text: item.text });
109-
open = false;
108+
const shouldContinue = dispatch(
109+
"close",
110+
{ index: item.index, text: item.text },
111+
{ cancelable: true },
112+
);
113+
if (shouldContinue) {
114+
open = false;
115+
}
110116
};
111117
112118
/**
@@ -199,8 +205,10 @@
199205
on:click={({ target }) => {
200206
if (buttonRef && buttonRef.contains(target)) return;
201207
if (menuRef && !menuRef.contains(target)) {
202-
dispatch("close");
203-
open = false;
208+
const shouldContinue = dispatch("close", null, { cancelable: true });
209+
if (shouldContinue) {
210+
open = false;
211+
}
204212
}
205213
}}
206214
/>
@@ -224,7 +232,12 @@
224232
on:click={({ target }) => {
225233
if (!(menuRef && menuRef.contains(target))) {
226234
open = !open;
227-
if (!open) dispatch("close");
235+
if (!open) {
236+
const shouldContinue = dispatch("close", null, { cancelable: true });
237+
if (!shouldContinue) {
238+
open = true;
239+
}
240+
}
228241
}
229242
}}
230243
on:mouseover
@@ -237,9 +250,11 @@
237250
e.preventDefault();
238251
} else if (e.key === "Escape") {
239252
e.stopPropagation();
240-
dispatch("close");
241-
open = false;
242-
buttonRef.focus();
253+
const shouldContinue = dispatch("close", null, { cancelable: true });
254+
if (shouldContinue) {
255+
open = false;
256+
buttonRef.focus();
257+
}
243258
}
244259
}
245260
}}

src/OverflowMenu/OverflowMenuItem.svelte

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
<script>
2+
/**
3+
* @event {MouseEvent} click
4+
*/
5+
26
/**
37
* Specify the item text.
48
* Alternatively, use the default slot
@@ -29,8 +33,9 @@
2933
/** Obtain a reference to the HTML element */
3034
export let ref = null;
3135
32-
import { afterUpdate, getContext } from "svelte";
36+
import { afterUpdate, createEventDispatcher, getContext } from "svelte";
3337
38+
const dispatch = createEventDispatcher();
3439
const { focusedId, add, update, change, items } = getContext("OverflowMenu");
3540
3641
$: item = $items.find((_) => _.id === id);
@@ -52,6 +57,17 @@
5257
href: href ? href : undefined,
5358
title: requireTitle ? ($$slots.default ? undefined : text) : undefined,
5459
};
60+
61+
function handleClick(e) {
62+
e.stopPropagation();
63+
64+
const shouldContinue = dispatch("click", e, { cancelable: true });
65+
66+
// Only update (close menu) if preventDefault was not called.
67+
if (shouldContinue) {
68+
update(id, item);
69+
}
70+
}
5571
</script>
5672

5773
<li
@@ -69,11 +85,7 @@
6985
<a
7086
bind:this={ref}
7187
{...buttonProps}
72-
on:click
73-
on:click={(e) => {
74-
e.stopPropagation();
75-
update(id, item);
76-
}}
88+
on:click={handleClick}
7789
on:keydown
7890
on:keydown={({ key }) => {
7991
if (key === "ArrowDown") {
@@ -93,11 +105,7 @@
93105
<button
94106
bind:this={ref}
95107
{...buttonProps}
96-
on:click
97-
on:click={(e) => {
98-
e.stopPropagation();
99-
update(id, item);
100-
}}
108+
on:click={handleClick}
101109
on:keydown
102110
on:keydown={({ key }) => {
103111
if (key === "ArrowDown") {
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<script lang="ts">
2+
import { OverflowMenu, OverflowMenuItem } from "carbon-components-svelte";
3+
</script>
4+
5+
<OverflowMenu
6+
on:close={(e) => {
7+
console.log("close", e.detail);
8+
}}
9+
>
10+
<OverflowMenuItem
11+
text="Manage credentials"
12+
on:click={(e) => {
13+
console.log("click", "Manage credentials");
14+
e.preventDefault(); // Prevent menu from closing
15+
}}
16+
/>
17+
<OverflowMenuItem
18+
href="https://cloud.ibm.com/docs/api-gateway/"
19+
text="API documentation"
20+
on:click={(e) => {
21+
console.log("click", "API documentation");
22+
}}
23+
/>
24+
<OverflowMenuItem
25+
danger
26+
text="Delete service"
27+
on:click={(e) => {
28+
console.log("click", "Delete service");
29+
}}
30+
/>
31+
</OverflowMenu>

tests/OverflowMenu/OverflowMenu.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { render, screen } from "@testing-library/svelte";
22
import { user } from "../setup-tests";
3+
import OverflowMenuPreventDefault from "./OverflowMenu.preventDefault.test.svelte";
34
import OverflowMenu from "./OverflowMenu.test.svelte";
45

56
describe("OverflowMenu", () => {
@@ -287,4 +288,39 @@ describe("OverflowMenu", () => {
287288

288289
expect(spy).toHaveBeenCalledWith("close", null);
289290
});
291+
292+
it("supports preventDefault on item to prevent menu from closing", async () => {
293+
const consoleLog = vi.spyOn(console, "log");
294+
render(OverflowMenuPreventDefault);
295+
296+
const menuButton = screen.getByRole("button");
297+
await user.click(menuButton);
298+
expect(menuButton).toHaveAttribute("aria-expanded", "true");
299+
300+
const menuItems = screen.getAllByRole("menuitem");
301+
await user.click(menuItems[0]);
302+
303+
expect(consoleLog).toHaveBeenCalledWith("click", "Manage credentials");
304+
expect(consoleLog).not.toHaveBeenCalledWith("close", {
305+
index: 0,
306+
text: "Manage credentials",
307+
});
308+
309+
expect(menuButton).toHaveAttribute("aria-expanded", "true");
310+
expect(screen.queryByRole("menu")).toBeInTheDocument();
311+
});
312+
313+
it("closes menu normally when preventDefault is not called", async () => {
314+
render(OverflowMenu);
315+
316+
const menuButton = screen.getByRole("button");
317+
await user.click(menuButton);
318+
expect(menuButton).toHaveAttribute("aria-expanded", "true");
319+
320+
const menuItems = screen.getAllByRole("menuitem");
321+
await user.click(menuItems[0]);
322+
323+
expect(menuButton).toHaveAttribute("aria-expanded", "false");
324+
expect(screen.queryByRole("menu")).not.toBeInTheDocument();
325+
});
290326
});

types/OverflowMenu/OverflowMenuItem.svelte.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,6 @@ export type OverflowMenuItemProps = Omit<$RestProps, keyof $Props> & $Props;
6666

6767
export default class OverflowMenuItem extends SvelteComponentTyped<
6868
OverflowMenuItemProps,
69-
{ click: WindowEventMap["click"]; keydown: WindowEventMap["keydown"] },
69+
{ click: CustomEvent<MouseEvent>; keydown: WindowEventMap["keydown"] },
7070
{ default: Record<string, never> }
7171
> {}

0 commit comments

Comments
 (0)