Skip to content

Commit 2747745

Browse files
committed
ensure that you can not click disabled items
1 parent b229857 commit 2747745

File tree

3 files changed

+108
-7
lines changed

3 files changed

+108
-7
lines changed

packages/@headlessui-react/src/components/menu/menu.test.tsx

Lines changed: 63 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -643,7 +643,9 @@ describe('Keyboard interactions', () => {
643643
<Menu.Item as="button" onClick={clickHandler}>
644644
Item B
645645
</Menu.Item>
646-
<Menu.Item as="a">Item C</Menu.Item>
646+
<Menu.Item>
647+
<button onClick={clickHandler}>Item C</button>
648+
</Menu.Item>
647649
</Menu.Items>
648650
</Menu>
649651
)
@@ -673,6 +675,18 @@ describe('Keyboard interactions', () => {
673675

674676
// Verify the button got "clicked"
675677
expect(clickHandler).toHaveBeenCalledTimes(1)
678+
679+
// Click the menu button again
680+
await click(getMenuButton())
681+
682+
// Active the last menu item
683+
await hover(getMenuItems()[2])
684+
685+
// Close menu, and invoke the item
686+
await press(Keys.Enter)
687+
688+
// Verify the button got "clicked"
689+
expect(clickHandler).toHaveBeenCalledTimes(2)
676690
})
677691
)
678692

@@ -2467,6 +2481,48 @@ describe('Mouse interactions', () => {
24672481
})
24682482
)
24692483

2484+
it(
2485+
'should be possible to click a menu item, which closes the menu and invokes the @click handler',
2486+
suppressConsoleLogs(async () => {
2487+
const clickHandler = jest.fn()
2488+
render(
2489+
<Menu>
2490+
<Menu.Button>Trigger</Menu.Button>
2491+
<Menu.Items>
2492+
<Menu.Item as="a">alice</Menu.Item>
2493+
<Menu.Item as="button" onClick={clickHandler}>
2494+
bob
2495+
</Menu.Item>
2496+
<Menu.Item>
2497+
<button onClick={clickHandler}>charlie</button>
2498+
</Menu.Item>
2499+
</Menu.Items>
2500+
</Menu>
2501+
)
2502+
2503+
// Open menu
2504+
await click(getMenuButton())
2505+
assertMenu(getMenu(), { state: MenuState.Open })
2506+
2507+
// We should be able to click the first item
2508+
await click(getMenuItems()[1])
2509+
assertMenu(getMenu(), { state: MenuState.Closed })
2510+
2511+
// Verify the callback has been called
2512+
expect(clickHandler).toHaveBeenCalledTimes(1)
2513+
2514+
// Let's re-open the window for now
2515+
await click(getMenuButton())
2516+
2517+
// Click the last item, which should close and invoke the handler
2518+
await click(getMenuItems()[2])
2519+
assertMenu(getMenu(), { state: MenuState.Closed })
2520+
2521+
// Verify the callback has been called
2522+
expect(clickHandler).toHaveBeenCalledTimes(2)
2523+
})
2524+
)
2525+
24702526
it(
24712527
'should be possible to click a disabled menu item, which is a no-op',
24722528
suppressConsoleLogs(async () => {
@@ -2567,8 +2623,8 @@ describe('Mouse interactions', () => {
25672623
<Menu.Item as="a" onClick={clickHandler} disabled>
25682624
bob
25692625
</Menu.Item>
2570-
<Menu.Item as="a" onClick={clickHandler}>
2571-
charlie
2626+
<Menu.Item disabled>
2627+
<button onClick={clickHandler}>charlie</button>
25722628
</Menu.Item>
25732629
</Menu.Items>
25742630
</Menu>
@@ -2581,9 +2637,11 @@ describe('Mouse interactions', () => {
25812637
const items = getMenuItems()
25822638

25832639
await focus(items[0])
2584-
await focus(items[1])
2585-
await press(Keys.Enter)
2640+
await click(items[1])
2641+
expect(clickHandler).not.toHaveBeenCalled()
25862642

2643+
// Activate the last item
2644+
await click(getMenuItems()[2])
25872645
expect(clickHandler).not.toHaveBeenCalled()
25882646
})
25892647
)

packages/@headlessui-react/src/utils/render.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,51 @@ export function render<TTag extends React.ElementType, TBag>(
3232
resolvedChildren,
3333

3434
// Filter out undefined values so that they don't override the existing values
35-
compact(passThroughProps)
35+
mergeEventFunctions(compact(passThroughProps), resolvedChildren.props, ['onClick'])
3636
)
3737
}
3838
}
3939

4040
return React.createElement(Component, passThroughProps, resolvedChildren)
4141
}
4242

43+
/**
44+
* We can use this function for the following useCase:
45+
*
46+
* <Menu.Item> <button onClick={console.log} /> </Menu.Item>
47+
*
48+
* Our `Menu.Item` will have an internal `onClick`, if you passthrough an `onClick` to the actual
49+
* `Menu.Item` component we will call it correctly. However, when we have an `onClick` on the actual
50+
* first child, that one should _also_ be called (but before this implementation, it was just
51+
* overriding the `onClick`). But it is only when we *render* that we have access to the existing
52+
* props of this component.
53+
*
54+
* It's a bit hacky, and not that clean, but it is something internal and we have tests to rely on
55+
* so that we can refactor this later (if needed).
56+
*/
57+
function mergeEventFunctions(
58+
passThroughProps: Record<string, any>,
59+
existingProps: Record<string, any>,
60+
functionsToMerge: string[]
61+
) {
62+
let clone = Object.assign({}, passThroughProps)
63+
for (let func of functionsToMerge) {
64+
if (passThroughProps[func] !== undefined && existingProps[func] !== undefined) {
65+
Object.assign(clone, {
66+
[func](event: { defaultPrevented: boolean }) {
67+
// Props we control
68+
if (!event.defaultPrevented) passThroughProps[func](event)
69+
70+
// Existing props on the component
71+
if (!event.defaultPrevented) existingProps[func](event)
72+
},
73+
})
74+
}
75+
}
76+
77+
return clone
78+
}
79+
4380
/**
4481
* This is a hack, but basically we want to keep the full 'API' of the component, but we do want to
4582
* wrap it in a forwardRef so that we _can_ passthrough the ref

packages/@headlessui-vue/src/components/menu/menu.test.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2526,7 +2526,9 @@ describe('Mouse interactions', () => {
25262526
<MenuItem as="a" @click="clickHandler" disabled>
25272527
bob
25282528
</MenuItem>
2529-
<MenuItem as="a" @click="clickHandler">charlie</MenuItem>
2529+
<MenuItem>
2530+
<button @click="clickHandler">charlie</button>
2531+
</MenuItem>
25302532
</MenuItems>
25312533
</Menu>
25322534
`,
@@ -2542,7 +2544,11 @@ describe('Mouse interactions', () => {
25422544
await focus(items[0])
25432545
await focus(items[1])
25442546
await press(Keys.Enter)
2547+
expect(clickHandler).not.toHaveBeenCalled()
25452548

2549+
// Activate the last item
2550+
await focus(items[2])
2551+
await press(Keys.Enter)
25462552
expect(clickHandler).not.toHaveBeenCalled()
25472553
})
25482554
})

0 commit comments

Comments
 (0)