Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions .claude/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,7 @@
"Bash(yarn test:*)",
"Bash(yarn lint:*)",
"Bash(yarn typecheck)",
"Bash(git add:*)",
"mcp__Context7__resolve-library-id",
"mcp__Context7__get-library-docs"
"Bash(git add:*)"
]
},
"hooks": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,13 @@ export function GroupSettingsDialog({ groupId, onClose }: GroupSettingsDialogPro

const handleLeaveGroup = async () => {
try {
const isCurrentlyOnAFileInThisGroup =
currentFileId && app.getFile(currentFileId)?.owningGroupId === groupId
await app.z.mutate.leaveGroup({ groupId }).client
onClose()
if (isCurrentlyOnAFileInThisGroup) {
navigate('/')
}
} catch (error) {
console.error('Error leaving group:', error)
}
Expand Down
64 changes: 64 additions & 0 deletions apps/examples/e2e/tests/test-page-menu.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,45 @@ import { setupOrReset, sleep } from '../shared-e2e'

declare const editor: Editor

const PAGE_MENU_ITEM_HEIGHT = 36
// The list reserves a few pixels of breathing room below the last row; the
// auto-fit and minimum heights both include it, so the e2e expectations must too.
const LIST_BOTTOM_PADDING = 4
const MIN_PAGE_MENU_LIST_HEIGHT = PAGE_MENU_ITEM_HEIGHT + LIST_BOTTOM_PADDING

const isMobileProject = () => test.info().project.name.includes('Mobile')

async function expectPageItemToBeCurrent(pageItem: Locator, isCurrent: boolean) {
await expect(pageItem).toHaveAttribute('data-iscurrent', isCurrent ? 'true' : 'false')
}

async function getPageMenuListHeight(pageList: Locator) {
return await pageList.evaluate((elm) => elm.getBoundingClientRect().height)
}

async function expectPageMenuListHeight(pageList: Locator, expectedHeight: number, tolerance = 1) {
await expect
.poll(async () => getPageMenuListHeight(pageList))
.toBeGreaterThanOrEqual(expectedHeight - tolerance)

await expect
.poll(async () => getPageMenuListHeight(pageList))
.toBeLessThanOrEqual(expectedHeight + tolerance)
}

async function dragPageMenuResizeHandle(page: Page, deltaY: number) {
const resizeHandle = page.locator('.tlui-page-menu__resize-handle')
const box = await resizeHandle.boundingBox()
if (!box) throw new Error('Could not find page menu resize handle')
const x = box.x + box.width / 2
const y = box.y + box.height / 2

await page.mouse.move(x, y)
await page.mouse.down()
await page.mouse.move(x, y + deltaY, { steps: 4 })
await page.mouse.up()
}

async function createPagesForReordering(page: Page) {
await page.evaluate(() => {
editor.createPage({ name: 'Page 2' })
Expand Down Expand Up @@ -49,6 +82,37 @@ test.describe('page menu', () => {
await expect(header).toBeHidden()
})

test('The page list height fits its rows when opened, resized, and reset', async ({
page,
pageMenu,
}) => {
test.skip(isMobileProject(), 'Desktop page menu resize behavior')

await page.evaluate(() => {
window.localStorage.removeItem('tldraw_page_menu_list_height')
editor.createPage({ name: 'Page 2' })
editor.createPage({ name: 'Page 3' })
})

await pageMenu.pagemenuButton.click()
await expect(pageMenu.pageItems).toHaveCount(3)

const autoFitHeight = PAGE_MENU_ITEM_HEIGHT * 3 + LIST_BOTTOM_PADDING
await expectPageMenuListHeight(pageMenu.pageList, autoFitHeight)

await dragPageMenuResizeHandle(page, 120)
await expect
.poll(async () => getPageMenuListHeight(pageMenu.pageList))
.toBeGreaterThan(autoFitHeight)

await dragPageMenuResizeHandle(page, -300)
await expectPageMenuListHeight(pageMenu.pageList, MIN_PAGE_MENU_LIST_HEIGHT)

await dragPageMenuResizeHandle(page, 120)
await page.locator('.tlui-page-menu__resize-handle').dblclick()
await expectPageMenuListHeight(pageMenu.pageList, autoFitHeight)
})

test('You can change pages', async ({ page, pageMenu }) => {
const { pagemenuButton, pageItems } = pageMenu

Expand Down
1 change: 1 addition & 0 deletions assets/translations/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,7 @@
"context-menu.reorder": "Reorder",
"page-menu.title": "Pages",
"page-menu.create-new-page": "Create new page",
"page-menu.max-pages-reached": "Maximum pages reached",
"page-menu.new-page-initial-name": "Page 1",
"page-menu.edit-start": "Edit",
"page-menu.edit-done": "Done",
Expand Down
4 changes: 0 additions & 4 deletions context7.json

This file was deleted.

2 changes: 1 addition & 1 deletion packages/tldraw/api-report.api.md

Large diffs are not rendered by default.

26 changes: 20 additions & 6 deletions packages/tldraw/src/lib/ui.css
Original file line number Diff line number Diff line change
Expand Up @@ -813,27 +813,41 @@

.tlui-page-menu__list__content {
position: relative;
overflow: hidden;
}

.tlui-page-menu__resize-handle {
flex: 0 0 auto;
position: relative;
height: 7px;
margin-bottom: -3px;
height: 1px;
cursor: ns-resize;
touch-action: none;
background: var(--tl-color-divider);
transition: background-color ease-in-out 150ms 80ms;
z-index: 999;
}

/* Hit test area */
.tlui-page-menu__resize-handle::after {
content: '';
position: absolute;
inset: -4px 0;
background: none;
z-index: 2000;
pointer-events: all;
}

/* Thick blue indicator */
.tlui-page-menu__resize-handle::before {
content: '';
position: absolute;
top: 3px;
top: 0;
left: 0;
right: 0;
height: 1px;
background: var(--tl-color-divider);
transition: background-color 100ms;
height: 1.5px;
background: transparent;
pointer-events: none;
z-index: 2001;
}

.tlui-page-menu__resize-handle[data-resizing='true']::before {
Expand Down
58 changes: 31 additions & 27 deletions packages/tldraw/src/lib/ui/components/PageMenu/DefaultPageMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,13 @@ import { PageItemInput } from './PageItemInput'
import { PageItemSubmenu } from './PageItemSubmenu'

const PAGE_MENU_LIST_HEIGHT_KEY = 'tldraw_page_menu_list_height'
const MIN_PAGE_MENU_LIST_HEIGHT = 54
const MAX_PAGE_MENU_RENDER_HEIGHT = 800
// Bottom padding included in the absolutely-positioned content stack — so the
// auto-fit list height should mirror it.
const PAGE_MENU_LIST_CONTENT_BOTTOM_PADDING = 4
const LIST_BOTTOM_PADDING = 4
const MAX_PAGE_MENU_AVAILABLE_HEIGHT_RATIO = 0.62
const PAGE_MENU_CREATE_BUTTON_HEIGHT = 40
const PAGE_MENU_RESIZE_HANDLE_HEIGHT = 7
const PAGE_MENU_RESIZE_HANDLE_HEIGHT = 1
const PAGE_MENU_ITEM_HEIGHT = 36
const MIN_PAGE_MENU_LIST_HEIGHT = PAGE_MENU_ITEM_HEIGHT + LIST_BOTTOM_PADDING
const PAGE_MENU_DRAG_THRESHOLD = 5
const PAGE_MENU_AUTO_SCROLL_ZONE = 16
const PAGE_MENU_AUTO_SCROLL_RAMP_DISTANCE = 48
Expand All @@ -53,17 +51,22 @@ function readSavedPageMenuListHeight(): number | null {
}
}

function getPageMenuRenderCap(availableHeight: number, hasFooter: boolean): number {
function getPageMenuRenderCap(availableHeight: number): number {
const maxMenuHeight = Math.min(
MAX_PAGE_MENU_RENDER_HEIGHT,
availableHeight * MAX_PAGE_MENU_AVAILABLE_HEIGHT_RATIO
)
const footerHeight = hasFooter
? PAGE_MENU_CREATE_BUTTON_HEIGHT + PAGE_MENU_RESIZE_HANDLE_HEIGHT
: 0
const footerHeight = PAGE_MENU_CREATE_BUTTON_HEIGHT + PAGE_MENU_RESIZE_HANDLE_HEIGHT
return Math.max(MIN_PAGE_MENU_LIST_HEIGHT, maxMenuHeight - footerHeight)
}

function getPageMenuAutoFitListHeight(pageCount: number): number {
return Math.max(
MIN_PAGE_MENU_LIST_HEIGHT,
pageCount * PAGE_MENU_ITEM_HEIGHT + LIST_BOTTOM_PADDING
)
}

/** @public @react */
export const DefaultPageMenu = memo(function DefaultPageMenu() {
const editor = useEditor()
Expand Down Expand Up @@ -149,10 +152,13 @@ export const DefaultPageMenu = memo(function DefaultPageMenu() {
editor.timers.requestAnimationFrame(updateAvailableHeight)
}, [editor, isOpen, updateAvailableHeight])

const renderCap = getPageMenuRenderCap(availableHeight, !isReadonlyMode)
const autoFitListHeight =
pages.length * PAGE_MENU_ITEM_HEIGHT + PAGE_MENU_LIST_CONTENT_BOTTOM_PADDING
const renderCap = getPageMenuRenderCap(availableHeight)
const autoFitListHeight = getPageMenuAutoFitListHeight(pages.length)
const renderedListHeight = Math.min(userListHeight ?? autoFitListHeight, renderCap)
const hasReachedMaxPages = pages.length >= editor.options.maxPages
const createPageButtonLabel = msg(
hasReachedMaxPages ? 'page-menu.max-pages-reached' : 'page-menu.create-new-page'
)

const handleResizePointerDown = useCallback((e: React.PointerEvent<HTMLDivElement>) => {
e.preventDefault()
Expand Down Expand Up @@ -537,7 +543,7 @@ export const DefaultPageMenu = memo(function DefaultPageMenu() {
<div
className="tlui-page-menu__list__content"
data-dragging={dragState !== null}
style={{ height: PAGE_MENU_ITEM_HEIGHT * pages.length + 4 }}
style={{ height: autoFitListHeight }}
>
{pages.map((page, index) => {
const isCurrentPage = page.id === currentPage.id
Expand Down Expand Up @@ -678,20 +684,18 @@ export const DefaultPageMenu = memo(function DefaultPageMenu() {
aria-orientation="horizontal"
aria-label={msg('page-menu.resize')}
/>
{!isReadonlyMode && (
<TldrawUiButton
type="menu"
className="tlui-page-menu__create-button"
data-testid="page-menu.create"
tooltip={msg('page-menu.create-new-page')}
title={msg('page-menu.create-new-page')}
disabled={pages.length >= editor.options.maxPages}
onClick={handleCreatePageClick}
>
<TldrawUiButtonLabel>{msg('page-menu.create-new-page')}</TldrawUiButtonLabel>
<TldrawUiButtonIcon icon="plus" small />
</TldrawUiButton>
)}
<TldrawUiButton
type="menu"
className="tlui-page-menu__create-button"
data-testid="page-menu.create"
tooltip={createPageButtonLabel}
title={createPageButtonLabel}
disabled={isReadonlyMode || hasReachedMaxPages}
onClick={handleCreatePageClick}
>
<TldrawUiButtonLabel>{createPageButtonLabel}</TldrawUiButtonLabel>
{!hasReachedMaxPages && <TldrawUiButtonIcon icon="plus" small />}
</TldrawUiButton>
</div>
</TldrawUiPopoverContent>
</TldrawUiPopover>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,7 @@ export type TLUiTranslationKey =
| 'context-menu.reorder'
| 'page-menu.title'
| 'page-menu.create-new-page'
| 'page-menu.max-pages-reached'
| 'page-menu.new-page-initial-name'
| 'page-menu.edit-start'
| 'page-menu.edit-done'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,7 @@ export const DEFAULT_TRANSLATION = {
'context-menu.reorder': 'Reorder',
'page-menu.title': 'Pages',
'page-menu.create-new-page': 'Create new page',
'page-menu.max-pages-reached': 'Maximum pages reached',
'page-menu.new-page-initial-name': 'Page 1',
'page-menu.edit-start': 'Edit',
'page-menu.edit-done': 'Done',
Expand Down
Loading