fix(keyboard): match Figma zoom behavior
This commit is contained in:
parent
992102f3a5
commit
85eb57559e
|
|
@ -1,8 +1,16 @@
|
|||
import type { Editor } from '@open-pencil/core/editor'
|
||||
|
||||
export function createProfilerActions(editor: Editor) {
|
||||
function activeCanvas() {
|
||||
return (
|
||||
document.querySelector<HTMLCanvasElement>(
|
||||
'[data-active-pane="true"] [data-test-id="canvas-element"]'
|
||||
) ?? document.querySelector<HTMLCanvasElement>('[data-test-id="canvas-element"]')
|
||||
)
|
||||
}
|
||||
|
||||
function viewportScreenCenter() {
|
||||
const canvas = document.querySelector<HTMLCanvasElement>('[data-test-id="canvas-element"]')
|
||||
const canvas = activeCanvas()
|
||||
if (canvas) {
|
||||
const rect = canvas.getBoundingClientRect()
|
||||
return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 }
|
||||
|
|
@ -11,7 +19,7 @@ export function createProfilerActions(editor: Editor) {
|
|||
}
|
||||
|
||||
function viewportCanvasCenter() {
|
||||
const canvas = document.querySelector<HTMLCanvasElement>('[data-test-id="canvas-element"]')
|
||||
const canvas = activeCanvas()
|
||||
if (canvas) {
|
||||
const rect = canvas.getBoundingClientRect()
|
||||
return { x: rect.width / 2, y: rect.height / 2 }
|
||||
|
|
|
|||
|
|
@ -40,6 +40,13 @@ function commandShortcuts(...commands: EditorCommandId[]): ShortcutDefinition[]
|
|||
})
|
||||
}
|
||||
|
||||
function zoomAtViewportCenter(delta: number): ShortcutAction {
|
||||
return ({ store }) => {
|
||||
const center = store.viewportCanvasCenter()
|
||||
store.applyZoom(delta, center.x, center.y)
|
||||
}
|
||||
}
|
||||
|
||||
function opacityBindings(): ShortcutDefinition[] {
|
||||
return ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'].map((digit) => ({
|
||||
id: `selection-opacity-${digit}`,
|
||||
|
|
@ -141,21 +148,25 @@ export function registerKeyboardShortcuts(options: KeyboardShortcutOptions) {
|
|||
{
|
||||
id: 'zoom-in',
|
||||
keys: [appMenuTinykeysShortcut('zoom-in') ?? '$mod+Equal', '$mod+Shift+Equal'],
|
||||
run: ({ store }) => {
|
||||
const center = store.viewportScreenCenter()
|
||||
store.applyZoom(-100, center.x, center.y)
|
||||
},
|
||||
run: zoomAtViewportCenter(-100),
|
||||
global: true
|
||||
},
|
||||
{
|
||||
id: 'zoom-out',
|
||||
keys: appMenuTinykeysShortcut('zoom-out') ?? '$mod+Minus',
|
||||
run: ({ store }) => {
|
||||
const center = store.viewportScreenCenter()
|
||||
store.applyZoom(100, center.x, center.y)
|
||||
},
|
||||
run: zoomAtViewportCenter(100),
|
||||
global: true
|
||||
},
|
||||
{
|
||||
id: 'zoom-in-unmodified',
|
||||
keys: 'Shift+Equal',
|
||||
run: zoomAtViewportCenter(-100)
|
||||
},
|
||||
{
|
||||
id: 'zoom-out-unmodified',
|
||||
keys: 'Minus',
|
||||
run: zoomAtViewportCenter(100)
|
||||
},
|
||||
{
|
||||
id: 'close-tab',
|
||||
keys: appMenuTinykeysShortcut('close') ?? '$mod+KeyW',
|
||||
|
|
|
|||
|
|
@ -329,22 +329,27 @@ test.describe('zoom shortcuts', () => {
|
|||
})
|
||||
|
||||
const pressShortcut = async (shortcut: string, code: 'Equal' | 'Minus') => {
|
||||
const observedEvent = editor.page.evaluate(
|
||||
(expectedCode) =>
|
||||
new Promise<{ defaultPrevented: boolean; shiftKey: boolean }>((resolve) => {
|
||||
const handleKeydown = (event: KeyboardEvent) => {
|
||||
if (event.code !== expectedCode) return
|
||||
window.removeEventListener('keydown', handleKeydown)
|
||||
resolve({ defaultPrevented: event.defaultPrevented, shiftKey: event.shiftKey })
|
||||
}
|
||||
window.addEventListener('keydown', handleKeydown)
|
||||
}),
|
||||
code
|
||||
)
|
||||
await editor.page.evaluate((expectedCode) => {
|
||||
delete document.documentElement.dataset.observedShortcut
|
||||
const handleKeydown = (event: KeyboardEvent) => {
|
||||
if (event.code !== expectedCode) return
|
||||
window.removeEventListener('keydown', handleKeydown)
|
||||
document.documentElement.dataset.observedShortcut = JSON.stringify({
|
||||
defaultPrevented: event.defaultPrevented,
|
||||
shiftKey: event.shiftKey
|
||||
})
|
||||
}
|
||||
window.addEventListener('keydown', handleKeydown)
|
||||
}, code)
|
||||
|
||||
await editor.page.keyboard.press(shortcut)
|
||||
await editor.canvas.waitForRender()
|
||||
return { event: await observedEvent, zoom: await getZoom() }
|
||||
const event = await editor.page.evaluate(() => {
|
||||
const observed = document.documentElement.dataset.observedShortcut
|
||||
if (!observed) throw new Error('Expected shortcut keydown event')
|
||||
return JSON.parse(observed) as { defaultPrevented: boolean; shiftKey: boolean }
|
||||
})
|
||||
return { event, zoom: await getZoom() }
|
||||
}
|
||||
|
||||
const zoomedIn = await pressShortcut('ControlOrMeta+Equal', 'Equal')
|
||||
|
|
@ -359,6 +364,73 @@ test.describe('zoom shortcuts', () => {
|
|||
expect(zoomedOut.zoom).toBeLessThan(zoomedInWithShift.zoom)
|
||||
})
|
||||
|
||||
test('Figma plus and minus shortcuts zoom around the active canvas center', async () => {
|
||||
await editor.page.evaluate(() => {
|
||||
const store = window.openPencil?.getStore?.()
|
||||
if (!store) throw new Error('OpenPencil store not initialized')
|
||||
store.zoomTo100()
|
||||
store.splitPane(store.activePaneId.value, 'horizontal')
|
||||
})
|
||||
await expect(editor.page.locator('[data-active-pane]')).toHaveCount(2)
|
||||
const splitHandle = editor.page.locator('[data-split-id]').first()
|
||||
const splitHandleBounds = await splitHandle.boundingBox()
|
||||
if (!splitHandleBounds) throw new Error('Expected canvas split handle')
|
||||
await editor.page.mouse.move(
|
||||
splitHandleBounds.x,
|
||||
splitHandleBounds.y + splitHandleBounds.height / 2
|
||||
)
|
||||
await editor.page.mouse.down()
|
||||
await editor.page.mouse.move(
|
||||
splitHandleBounds.x + 100,
|
||||
splitHandleBounds.y + splitHandleBounds.height / 2
|
||||
)
|
||||
await editor.page.mouse.up()
|
||||
const activeCanvas = editor.page.locator('[data-active-pane="true"]')
|
||||
const inactiveCanvas = editor.page.locator('[data-active-pane="false"]')
|
||||
const activeBounds = await activeCanvas.boundingBox()
|
||||
const inactiveBounds = await inactiveCanvas.boundingBox()
|
||||
if (!activeBounds || !inactiveBounds) throw new Error('Expected canvas pane bounds')
|
||||
expect(activeBounds.width).toBeLessThan(inactiveBounds.width)
|
||||
|
||||
const centerBefore = await editor.page.evaluate(() => {
|
||||
const store = window.openPencil?.getStore?.()
|
||||
if (!store) throw new Error('OpenPencil store not initialized')
|
||||
const center = store.viewportCanvasCenter()
|
||||
return { center, canvasPoint: store.screenToCanvas(center.x, center.y) }
|
||||
})
|
||||
|
||||
expect(centerBefore.center.x).toBeCloseTo(activeBounds.width / 2)
|
||||
expect(centerBefore.center.y).toBeCloseTo(activeBounds.height / 2)
|
||||
|
||||
await editor.page.keyboard.press('Shift+Equal')
|
||||
await editor.canvas.waitForRender()
|
||||
const zoomedIn = await getZoom()
|
||||
const centerAfter = await editor.page.evaluate(() => {
|
||||
const store = window.openPencil?.getStore?.()
|
||||
if (!store) throw new Error('OpenPencil store not initialized')
|
||||
const center = store.viewportCanvasCenter()
|
||||
return { center, canvasPoint: store.screenToCanvas(center.x, center.y) }
|
||||
})
|
||||
|
||||
await editor.page.keyboard.press('Minus')
|
||||
await editor.canvas.waitForRender()
|
||||
|
||||
const inactivePaneId = await inactiveCanvas.getAttribute('data-pane-id')
|
||||
if (!inactivePaneId) throw new Error('Expected inactive canvas pane')
|
||||
await editor.page.evaluate((paneId) => {
|
||||
const store = window.openPencil?.getStore?.()
|
||||
if (!store) throw new Error('OpenPencil store not initialized')
|
||||
store.closePane(paneId)
|
||||
}, inactivePaneId)
|
||||
await expect(editor.page.locator('[data-active-pane]')).toHaveCount(1)
|
||||
|
||||
expect(centerAfter.center).toEqual(centerBefore.center)
|
||||
expect(centerAfter.canvasPoint.x).toBeCloseTo(centerBefore.canvasPoint.x)
|
||||
expect(centerAfter.canvasPoint.y).toBeCloseTo(centerBefore.canvasPoint.y)
|
||||
expect(zoomedIn).toBeGreaterThan(1)
|
||||
expect(await getZoom()).toBeLessThan(zoomedIn)
|
||||
})
|
||||
|
||||
test('⌘0 zooms to 100%', async () => {
|
||||
await editor.canvas.clearCanvas()
|
||||
await editor.canvas.drawRect(100, 100, 60, 60)
|
||||
|
|
|
|||
Loading…
Reference in a new issue