Skip to content

Commit 4170e5b

Browse files
authored
fix(tldraw): right-clicking a page item opens its submenu (tldraw#8927)
In order to let users right-click page items in the page menu without accidentally starting a drag-to-reorder gesture, this PR ignores non-primary buttons on the page item drag handle and wires the page item's `onContextMenu` to open the per-item submenu. The open submenu is tracked through the editor menu manager so it closes together with the page menu. ### Change type - [x] `bugfix` ### Test plan 1. Open the page menu with multiple pages. 2. Right-click on a page item. 3. Confirm the page item's submenu opens (duplicate, move up/down, delete, etc.). 4. Confirm the pages are not reordered as a result of the right-click. 5. Close the page menu and confirm any open per-item submenu is also closed. - [x] End to end tests ### Release notes - Fixed a bug where right-clicking a page item in the page menu could start a drag-to-reorder gesture instead of opening the item's submenu. ### API changes - none ### Code changes | Section | LOC change | | --------- | ---------- | | Core code | +40 / -1 | | Tests | +18 / -0 |
1 parent 9caa949 commit 4170e5b

3 files changed

Lines changed: 59 additions & 2 deletions

File tree

apps/examples/e2e/tests/test-page-menu.spec.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -456,6 +456,24 @@ test.describe('page menu', () => {
456456
})
457457

458458
test.describe('You can use the page menu', () => {
459+
test('Right clicking a page item opens its submenu without reordering pages', async ({
460+
page,
461+
pageMenu,
462+
}) => {
463+
test.skip(isMobileProject(), 'Mobile emulation does not simulate right-click')
464+
465+
await createPagesForReordering(page)
466+
await pageMenu.pagemenuButton.click()
467+
await expect(pageMenu.pageItems).toHaveCount(3)
468+
469+
const firstPageItem = await pageMenu.getPageItem(0)
470+
await firstPageItem.locator('.tlui-page-menu__item__button').click({ button: 'right' })
471+
472+
await expect(page.getByRole('menuitem', { name: /duplicate/i })).toBeVisible()
473+
await expect(firstPageItem).toHaveAttribute('data-dragging', 'false')
474+
expect(await getPageNames(page)).toEqual(['Page 1', 'Page 2', 'Page 3'])
475+
})
476+
459477
test('You can duplicate a page from the page menu', async ({ page, pageMenu }) => {
460478
const { pagemenuButton, pageItems } = pageMenu
461479

packages/tldraw/src/lib/ui/components/PageMenu/DefaultPageMenu.tsx

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,28 @@ export const DefaultPageMenu = memo(function DefaultPageMenu() {
7474
// The id of the page currently being renamed inline, if any.
7575
const [editingPageId, setEditingPageId] = useState<TLPageId | null>(null)
7676

77-
const handleOpenChange = useCallback(() => setEditingPageId(null), [])
77+
const closePageItemSubmenus = useCallback(
78+
(exceptIndex?: number) => {
79+
const contextSuffix = `-${editor.contextId}`
80+
for (const menuId of editor.menus.getOpenMenus()) {
81+
const id = menuId.endsWith(contextSuffix) ? menuId.slice(0, -contextSuffix.length) : menuId
82+
if (!id.startsWith('page item submenu ')) continue
83+
if (exceptIndex !== undefined && id === `page item submenu ${exceptIndex}`) continue
84+
editor.menus.deleteOpenMenu(id)
85+
}
86+
},
87+
[editor]
88+
)
89+
90+
const handleOpenChange = useCallback(
91+
(isOpen: boolean) => {
92+
setEditingPageId(null)
93+
if (!isOpen) {
94+
closePageItemSubmenus()
95+
}
96+
},
97+
[closePageItemSubmenus]
98+
)
7899

79100
const [isOpen, onOpenChange] = useMenuIsOpen('page-menu', handleOpenChange)
80101

@@ -342,6 +363,8 @@ export const DefaultPageMenu = memo(function DefaultPageMenu() {
342363
}, [editor, tickAutoScrollDuringDrag])
343364

344365
const handlePointerDown = useCallback((e: React.PointerEvent<HTMLButtonElement>) => {
366+
if (e.button !== 0) return
367+
345368
const { clientY, currentTarget } = e
346369
const { id, index } = currentTarget.dataset
347370
if (!id || index === undefined) return
@@ -400,6 +423,17 @@ export const DefaultPageMenu = memo(function DefaultPageMenu() {
400423
[editor, trackEvent]
401424
)
402425

426+
const handleItemContextMenu = useCallback(
427+
(e: React.MouseEvent<HTMLDivElement>, index: number) => {
428+
e.preventDefault()
429+
e.stopPropagation()
430+
431+
closePageItemSubmenus(index)
432+
editor.menus.addOpenMenu(`page item submenu ${index}`)
433+
},
434+
[closePageItemSubmenus, editor]
435+
)
436+
403437
const handlePointerCancel = useCallback((e: React.PointerEvent<HTMLButtonElement>) => {
404438
const mut = rMutables.current
405439
releasePointerCapture(e.currentTarget, e)
@@ -533,6 +567,11 @@ export const DefaultPageMenu = memo(function DefaultPageMenu() {
533567
data-dragging={isDragging}
534568
data-editing={isRenamingThisPage}
535569
className="tlui-page-menu__item"
570+
onContextMenu={
571+
!isReadonlyMode && !isRenamingThisPage
572+
? (e) => handleItemContextMenu(e, index)
573+
: undefined
574+
}
536575
style={{
537576
zIndex: isDragging
538577
? pages.length + 2

packages/tldraw/src/lib/ui/components/PageMenu/PageItemSubmenu.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ export const PageItemSubmenu = track(function PageItemSubmenu({
6464
<TldrawUiButtonIcon icon="dots-vertical" small />
6565
</TldrawUiButton>
6666
</TldrawUiDropdownMenuTrigger>
67-
<TldrawUiDropdownMenuContent alignOffset={0} side="right" sideOffset={-4}>
67+
<TldrawUiDropdownMenuContent side="bottom" align="start" alignOffset={0} sideOffset={0}>
6868
<TldrawUiMenuContextProvider type="menu" sourceId="page-menu">
6969
<TldrawUiMenuGroup id="modify">
7070
{onRename && (

0 commit comments

Comments
 (0)