Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commitaf5b0b4

Browse files
authored
EnsurebuttonRef.current.click() works (#3768)
This PR fixes an issue where adding a ref to the `<MenuButtonref={btnRef}>` and later calling `btnRef.current.click()` would not openthe `Menu`.This is happening because recently we started using `pointerdown`instead of `click` to open the `Menu`. So the only way to open the`Menu` programmatically is to call `btnRef.current.dispatchEvent(newPointerEvent('pointerdown', { bubbles: true }))` which is a bit of amouthful.We also recently fixed an issue where the `Listbox` would immediatelyclose after opening the Listbox on touch devices. That solution requiredus to not only handle the `pointerdown` event but also the `click`event.So if anything, this PR makes the code more consistent between the`Menu` and `Listbox` component behavior and in turn solves this`ref.current.click()` issue.This PR also does some internal refactoring to make the code a bitcleaner.Fixes:#3749
1 parenta12f9f2 commitaf5b0b4

File tree

5 files changed

+100
-23
lines changed

5 files changed

+100
-23
lines changed

‎packages/@headlessui-react/CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
- Fix incorrect double invocation of menu items, listbox options and combobox options ([#3766](https://github.com/tailwindlabs/headlessui/pull/3766))
1313
- Fix memory leak in SSR environment ([#3767](https://github.com/tailwindlabs/headlessui/pull/3767))
14+
- Ensure programmatic`.click()` on`MenuButton` ref works ([#3768](https://github.com/tailwindlabs/headlessui/pull/3768))
1415

1516
##[2.2.6] - 2025-07-24
1617

‎packages/@headlessui-react/src/components/listbox/listbox.test.tsx‎

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import{render,waitFor}from'@testing-library/react'
2-
importReact,{Fragment,createElement,useEffect,useState}from'react'
1+
import{act,render,waitFor}from'@testing-library/react'
2+
importReact,{Fragment,createElement,createRef,useEffect,useState}from'react'
33
import{
44
ListboxMode,
55
ListboxState,
@@ -1233,6 +1233,40 @@ describe('Rendering', () => {
12331233
expect(handleChange).toHaveBeenNthCalledWith(2,'bob')
12341234
})
12351235
})
1236+
1237+
it(
1238+
'should be possible to open a listbox programmatically via .click()',
1239+
suppressConsoleLogs(async()=>{
1240+
letbtnRef=createRef<HTMLButtonElement>()
1241+
1242+
render(
1243+
<Listbox>
1244+
<ListboxButtonref={btnRef}>Trigger</ListboxButton>
1245+
<ListboxOptions>
1246+
<ListboxOptionvalue="a">Option A</ListboxOption>
1247+
<ListboxOptionvalue="b">Option B</ListboxOption>
1248+
<ListboxOptionvalue="c">Option C</ListboxOption>
1249+
</ListboxOptions>
1250+
</Listbox>
1251+
)
1252+
1253+
assertListboxButton({state:ListboxState.InvisibleUnmounted})
1254+
assertListbox({state:ListboxState.InvisibleUnmounted})
1255+
1256+
// Open listbox
1257+
act(()=>btnRef.current?.click())
1258+
1259+
// Verify it is open
1260+
assertListboxButton({state:ListboxState.Visible})
1261+
assertListbox({state:ListboxState.Visible})
1262+
assertListboxButtonLinkedWithListbox()
1263+
1264+
// Verify we have listbox options
1265+
letoptions=getListboxOptions()
1266+
expect(options).toHaveLength(3)
1267+
options.forEach((option)=>assertListboxOption(option))
1268+
})
1269+
)
12361270
})
12371271

12381272
describe('Rendering composition',()=>{

‎packages/@headlessui-react/src/components/listbox/listbox.tsx‎

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import React, {
1515
typeElementType,
1616
typeMutableRefObject,
1717
typeKeyboardEventasReactKeyboardEvent,
18+
typeMouseEventasReactMouseEvent,
1819
typePointerEventasReactPointerEvent,
1920
typeRef,
2021
}from'react'
@@ -435,12 +436,7 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
435436
}
436437
})
437438

438-
letpointerTypeRef=useRef<'touch'|'mouse'|'pen'|null>(null)
439-
lethandlePointerDown=useEvent((event:ReactPointerEvent)=>{
440-
pointerTypeRef.current=event.pointerType
441-
442-
if(event.pointerType!=='mouse')return
443-
439+
lettoggle=useEvent((event:ReactPointerEvent|ReactMouseEvent)=>{
444440
if(event.button!==MouseButton.Left)return// Only handle left clicks
445441
if(isDisabledReactIssue7711(event.currentTarget))returnevent.preventDefault()
446442
if(machine.state.listboxState===ListboxStates.Open){
@@ -452,18 +448,16 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
452448
}
453449
})
454450

455-
lethandleClick=useEvent((event:ReactPointerEvent)=>{
456-
if(pointerTypeRef.current==='mouse')return
451+
letpointerTypeRef=useRef<'touch'|'mouse'|'pen'|null>(null)
452+
lethandlePointerDown=useEvent((event:ReactPointerEvent)=>{
453+
pointerTypeRef.current=event.pointerType
454+
if(event.pointerType!=='mouse')return
455+
toggle(event)
456+
})
457457

458-
if(event.button!==MouseButton.Left)return// Only handle left clicks
459-
if(isDisabledReactIssue7711(event.currentTarget))returnevent.preventDefault()
460-
if(machine.state.listboxState===ListboxStates.Open){
461-
flushSync(()=>machine.actions.closeListbox())
462-
machine.state.buttonElement?.focus({preventScroll:true})
463-
}else{
464-
event.preventDefault()
465-
machine.actions.openListbox({focus:Focus.Nothing})
466-
}
458+
lethandleClick=useEvent((event:ReactMouseEvent)=>{
459+
if(pointerTypeRef.current==='mouse')return
460+
toggle(event)
467461
})
468462

469463
// This is needed so that we can "cancel" the click event when we use the `Enter` key on a button.

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

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import{render,waitFor}from'@testing-library/react'
2-
importReact,{Fragment,createElement,useEffect}from'react'
1+
import{act,render,waitFor}from'@testing-library/react'
2+
importReact,{Fragment,createElement,createRef,useEffect}from'react'
33
import{
44
MenuState,
55
assertActiveElement,
@@ -542,6 +542,40 @@ describe('Rendering', () => {
542542
// Verify that the third menu item is active
543543
assertMenuLinkedWithMenuItem(items[2])
544544
})
545+
546+
it(
547+
'should be possible to open a menu programmatically via .click()',
548+
suppressConsoleLogs(async()=>{
549+
letbtnRef=createRef<HTMLButtonElement>()
550+
551+
render(
552+
<Menu>
553+
<MenuButtonref={btnRef}>Trigger</MenuButton>
554+
<MenuItems>
555+
<MenuItemas="a">Item A</MenuItem>
556+
<MenuItemas="a">Item B</MenuItem>
557+
<MenuItemas="a">Item C</MenuItem>
558+
</MenuItems>
559+
</Menu>
560+
)
561+
562+
assertMenuButton({state:MenuState.InvisibleUnmounted})
563+
assertMenu({state:MenuState.InvisibleUnmounted})
564+
565+
// Open menu
566+
act(()=>btnRef.current?.click())
567+
568+
// Verify it is open
569+
assertMenuButton({state:MenuState.Visible})
570+
assertMenu({state:MenuState.Visible})
571+
assertMenuButtonLinkedWithMenu()
572+
573+
// Verify we have menu items
574+
letitems=getMenuItems()
575+
expect(items).toHaveLength(3)
576+
items.forEach((item)=>assertMenuItem(item))
577+
})
578+
)
545579
})
546580

547581
describe('Rendering composition',()=>{

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

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ import {
7272
import{useDescriptions}from'../description/description'
7373
import{Keys}from'../keyboard'
7474
import{useLabelContext,useLabels}from'../label/label'
75+
import{MouseButton}from'../mouse'
7576
import{Portal}from'../portal/portal'
7677
import{ActionTypes,ActivationTrigger,MenuState,typeMenuItemDataRef}from'./menu-machine'
7778
import{MenuContext,useMenuMachine,useMenuMachineContext}from'./menu-machine-glue'
@@ -265,8 +266,8 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
265266
select:useCallback((target)=>target.click(),[]),
266267
})
267268

268-
lethandlePointerDown=useEvent((event:ReactPointerEvent)=>{
269-
if(event.button!==0)return// Only handle left clicks
269+
lettoggle=useEvent((event:ReactPointerEvent)=>{
270+
if(event.button!==MouseButton.Left)return// Only handle left clicks
270271
if(isDisabledReactIssue7711(event.currentTarget))returnevent.preventDefault()
271272
if(disabled)return
272273
if(menuState===MenuState.Open){
@@ -282,6 +283,18 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
282283
}
283284
})
284285

286+
letpointerTypeRef=useRef<'touch'|'mouse'|'pen'|null>(null)
287+
lethandlePointerDown=useEvent((event:ReactPointerEvent)=>{
288+
pointerTypeRef.current=event.pointerType
289+
if(event.pointerType!=='mouse')return
290+
toggle(event)
291+
})
292+
293+
lethandleClick=useEvent((event:ReactPointerEvent)=>{
294+
if(pointerTypeRef.current==='mouse')return
295+
toggle(event)
296+
})
297+
285298
let{isFocusVisible:focus, focusProps}=useFocusRing({ autoFocus})
286299
let{isHovered:hover, hoverProps}=useHover({isDisabled:disabled})
287300
let{pressed:active, pressProps}=useActivePress({ disabled})
@@ -311,6 +324,7 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
311324
onKeyDown:handleKeyDown,
312325
onKeyUp:handleKeyUp,
313326
onPointerDown:handlePointerDown,
327+
onClick:handleClick,
314328
},
315329
focusProps,
316330
hoverProps,

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp