snapshot of https://github.com/tldraw/tldraw.git (main, 2026-09-23; blobs > 131072B omitted)
Some checks failed
Deploy analytics worker / Deploy analytics worker to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
i18n upload strings / Upload latest strings (push) Failing after 1s
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
Deploy grafana resources / ${{ github.event_name == 'push' && 'Deploy grafana resources' || 'Validate grafana resources (dry run)' }} (push) Failing after 0s
End to end tests (dotcom) / Check for relevant changes (push) Failing after 0s
End to end tests (examples) / Check for relevant changes (push) Failing after 0s
End to end tests (mermaid readability) / Check for relevant changes (push) Failing after 1s
End to end tests (perf) / Check for relevant changes (push) Failing after 0s
Publish editor extensions / Publish editor extensions (push) Failing after 1s
Publish SDK packages / Remove the publish-packages label (push) Has been skipped
Checks / Tests & checks (push) Has been cancelled
Checks / Build all projects (push) Has been cancelled
Deploy templates / Deploy templates (push) Has been cancelled
End to end tests (dotcom) / End to end tests (dotcom) (push) Has been cancelled
End to end tests (mermaid readability) / End to end tests (mermaid readability) (push) Has been cancelled
End to end tests (perf) / End to end tests (perf) (push) Has been cancelled
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Has been cancelled
End to end tests (examples) / End to end tests (examples) (push) Has been cancelled
Publish SDK packages / Publish SDK packages (push) Has been cancelled
Publish SDK packages / Tell user released version (push) Has been cancelled
Publish SDK packages / Publishes code templates to separate repositories (push) Has been cancelled
Publish SDK packages / Deploys the template demos with the new SDK version (push) Has been cancelled
Some checks failed
Deploy analytics worker / Deploy analytics worker to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
i18n upload strings / Upload latest strings (push) Failing after 1s
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
Deploy grafana resources / ${{ github.event_name == 'push' && 'Deploy grafana resources' || 'Validate grafana resources (dry run)' }} (push) Failing after 0s
End to end tests (dotcom) / Check for relevant changes (push) Failing after 0s
End to end tests (examples) / Check for relevant changes (push) Failing after 0s
End to end tests (mermaid readability) / Check for relevant changes (push) Failing after 1s
End to end tests (perf) / Check for relevant changes (push) Failing after 0s
Publish editor extensions / Publish editor extensions (push) Failing after 1s
Publish SDK packages / Remove the publish-packages label (push) Has been skipped
Checks / Tests & checks (push) Has been cancelled
Checks / Build all projects (push) Has been cancelled
Deploy templates / Deploy templates (push) Has been cancelled
End to end tests (dotcom) / End to end tests (dotcom) (push) Has been cancelled
End to end tests (mermaid readability) / End to end tests (mermaid readability) (push) Has been cancelled
End to end tests (perf) / End to end tests (perf) (push) Has been cancelled
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Has been cancelled
End to end tests (examples) / End to end tests (examples) (push) Has been cancelled
Publish SDK packages / Publish SDK packages (push) Has been cancelled
Publish SDK packages / Tell user released version (push) Has been cancelled
Publish SDK packages / Publishes code templates to separate repositories (push) Has been cancelled
Publish SDK packages / Deploys the template demos with the new SDK version (push) Has been cancelled
This commit is contained in:
298
apps/examples/e2e/tests/test-focus.spec.ts
Normal file
298
apps/examples/e2e/tests/test-focus.spec.ts
Normal file
@@ -0,0 +1,298 @@
|
||||
import test, { expect } from '@playwright/test'
|
||||
import { Editor } from 'tldraw'
|
||||
|
||||
declare const editor: Editor
|
||||
declare const EDITOR_A: Editor
|
||||
declare const EDITOR_B: Editor
|
||||
declare const EDITOR_C: Editor
|
||||
|
||||
// We're just testing the events, not the actual results.
|
||||
|
||||
test.describe('Focus', () => {
|
||||
test('focus events', async ({ page }) => {
|
||||
await page.goto('http://localhost:5420/multiple/full')
|
||||
await page.waitForSelector('.tl-canvas')
|
||||
|
||||
const EditorA = (await page.$(`.A`))!
|
||||
const EditorB = (await page.$(`.B`))!
|
||||
const EditorC = (await page.$(`.C`))!
|
||||
expect(EditorA).toBeTruthy()
|
||||
expect(EditorB).toBeTruthy()
|
||||
expect(EditorC).toBeTruthy()
|
||||
|
||||
async function isOnlyFocused(id: 'A' | 'B' | 'C' | null) {
|
||||
let activeElement: string | null = null
|
||||
const isA = await EditorA.evaluate(
|
||||
(node) => document.activeElement === node || node.contains(document.activeElement)
|
||||
)
|
||||
const isB = await EditorB.evaluate(
|
||||
(node) => document.activeElement === node || node.contains(document.activeElement)
|
||||
)
|
||||
|
||||
const isC = await EditorC.evaluate(
|
||||
(node) => document.activeElement === node || node.contains(document.activeElement)
|
||||
)
|
||||
|
||||
activeElement = isA ? 'A' : isB ? 'B' : isC ? 'C' : null
|
||||
|
||||
expect(
|
||||
activeElement,
|
||||
`Active element should have been ${id}, but was ${activeElement ?? 'null'} instead`
|
||||
).toBe(id)
|
||||
|
||||
await page.evaluate(
|
||||
({ id }) => {
|
||||
if (
|
||||
!(
|
||||
EDITOR_A.getInstanceState().isFocused === (id === 'A') &&
|
||||
EDITOR_B.getInstanceState().isFocused === (id === 'B') &&
|
||||
EDITOR_C.getInstanceState().isFocused === (id === 'C')
|
||||
)
|
||||
) {
|
||||
throw Error('isFocused is not correct')
|
||||
}
|
||||
},
|
||||
{ id }
|
||||
)
|
||||
}
|
||||
|
||||
// Component A has autofocus
|
||||
// Component B does not
|
||||
// Component C does not
|
||||
// Component B and C share persistence id
|
||||
|
||||
await (await page.$('body'))?.click()
|
||||
|
||||
await isOnlyFocused('A')
|
||||
|
||||
await EditorA.click()
|
||||
|
||||
await isOnlyFocused('A')
|
||||
|
||||
await EditorA.click()
|
||||
|
||||
await isOnlyFocused('A')
|
||||
|
||||
await EditorB.click()
|
||||
|
||||
await isOnlyFocused('B')
|
||||
|
||||
// Escape does not break focus
|
||||
await page.keyboard.press('Escape')
|
||||
|
||||
await isOnlyFocused('B')
|
||||
})
|
||||
|
||||
test('kbds when not focused', async ({ page }) => {
|
||||
await page.goto('http://localhost:5420/multiple/full')
|
||||
await page.waitForSelector('.tl-canvas')
|
||||
|
||||
// Should not have any shapes on the page
|
||||
expect(await page.evaluate(() => EDITOR_A.getCurrentPageShapes().length)).toBe(0)
|
||||
|
||||
const EditorA = (await page.$(`.A`))!
|
||||
await page.keyboard.press('r')
|
||||
await EditorA.click({ position: { x: 100, y: 100 } })
|
||||
|
||||
// Should not have created a shape
|
||||
expect(await page.evaluate(() => EDITOR_A.getCurrentPageShapes().length)).toBe(1)
|
||||
|
||||
const TextArea = page.getByTestId(`textarea`)
|
||||
await TextArea.focus()
|
||||
await page.keyboard.type('hello world')
|
||||
await page.keyboard.press('Control+A')
|
||||
await page.keyboard.press('Delete')
|
||||
|
||||
// Should not have deleted the page
|
||||
expect(await page.evaluate(() => EDITOR_A.getCurrentPageShapes().length)).toBe(1)
|
||||
})
|
||||
|
||||
test('kbds when focused', async ({ page }) => {
|
||||
await page.goto('http://localhost:5420/multiple/full')
|
||||
await page.waitForSelector('.tl-canvas')
|
||||
|
||||
const EditorA = (await page.$(`.A`))!
|
||||
const EditorB = (await page.$(`.B`))!
|
||||
const EditorC = (await page.$(`.C`))!
|
||||
expect(EditorA).toBeTruthy()
|
||||
expect(EditorB).toBeTruthy()
|
||||
expect(EditorC).toBeTruthy()
|
||||
|
||||
await (await page.$('body'))?.click()
|
||||
|
||||
expect(await EditorA.$('.tlui-button[data-testid="tools.draw"][aria-pressed="true"]')).toBe(
|
||||
null
|
||||
)
|
||||
expect(await EditorB.$('.tlui-button[data-testid="tools.draw"][aria-pressed="true"]')).toBe(
|
||||
null
|
||||
)
|
||||
|
||||
await page.keyboard.press('d')
|
||||
|
||||
expect(await EditorA.$('.tlui-button[data-testid="tools.draw"][aria-pressed="true"]')).not.toBe(
|
||||
null
|
||||
)
|
||||
expect(await EditorB.$('.tlui-button[data-testid="tools.draw"][aria-pressed="true"]')).toBe(
|
||||
null
|
||||
)
|
||||
|
||||
await EditorB.click()
|
||||
await page.waitForTimeout(100) // takes 30ms or so to focus
|
||||
await page.keyboard.press('d')
|
||||
|
||||
expect(await EditorA.$('.tlui-button[data-testid="tools.draw"][aria-pressed="true"]')).not.toBe(
|
||||
null
|
||||
)
|
||||
expect(await EditorB.$('.tlui-button[data-testid="tools.draw"][aria-pressed="true"]')).not.toBe(
|
||||
null
|
||||
)
|
||||
})
|
||||
|
||||
test('kbds after clicking on ui elements', async ({ page }) => {
|
||||
await page.goto('http://localhost:5420/end-to-end')
|
||||
await page.waitForSelector('.tl-canvas')
|
||||
|
||||
const EditorA = (await page.$(`.tl-container`))!
|
||||
expect(EditorA).toBeTruthy()
|
||||
|
||||
const drawButton = await EditorA.$('.tlui-button[data-testid="tools.draw"]')
|
||||
|
||||
// select button should be selected, not the draw button
|
||||
expect(await EditorA.$('.tlui-button[data-testid="tools.draw"][aria-pressed="true"]')).toBe(
|
||||
null
|
||||
)
|
||||
|
||||
await drawButton?.click()
|
||||
|
||||
// draw button should be selected now
|
||||
expect(await EditorA.$('.tlui-button[data-testid="tools.draw"][aria-pressed="true"]')).not.toBe(
|
||||
null
|
||||
)
|
||||
|
||||
await page.keyboard.press('v')
|
||||
|
||||
// select button should be selected again
|
||||
expect(await EditorA.$('.tlui-button[data-testid="tools.draw"][aria-pressed="true"]')).toBe(
|
||||
null
|
||||
)
|
||||
})
|
||||
|
||||
test('still focuses text after clicking on style button', async ({ page }) => {
|
||||
await page.goto('http://localhost:5420/end-to-end')
|
||||
await page.waitForSelector('.tl-canvas')
|
||||
|
||||
const EditorA = (await page.$(`.tl-container`))!
|
||||
expect(EditorA).toBeTruthy()
|
||||
|
||||
// Create a new note, text should be focused
|
||||
await page.keyboard.press('n')
|
||||
await (await page.$('body'))?.click()
|
||||
await page.waitForSelector('.tl-shape')
|
||||
|
||||
const blueButton = await page.$('.tlui-button[data-testid="style.color.blue"]')
|
||||
await blueButton?.click()
|
||||
|
||||
// Text should still be focused.
|
||||
expect(
|
||||
await page.evaluate(() => !!document.querySelector('.tl-shape div[contenteditable]:focus'))
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
test('edit->edit, focus stays in the text areas when going from shape-to-shape', async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto('http://localhost:5420/end-to-end')
|
||||
await page.waitForSelector('.tl-canvas')
|
||||
|
||||
const EditorA = (await page.$(`.tl-container`))!
|
||||
expect(EditorA).toBeTruthy()
|
||||
|
||||
// Create a new note, text should be focused
|
||||
await page.keyboard.press('n')
|
||||
await (await page.$('body'))?.click()
|
||||
await page.waitForSelector('.tl-shape')
|
||||
await page.keyboard.type('test')
|
||||
const firstNoteId = await page.evaluate(() => editor.getEditingShapeId())
|
||||
const firstNote = page.locator(`.tl-shape[data-shape-id="${firstNoteId}"]`)
|
||||
|
||||
// create new note next to it
|
||||
await page.keyboard.press('Tab')
|
||||
await expect(page.locator('.tl-shape')).toHaveCount(2)
|
||||
|
||||
// Tab pans to the new note; let the camera settle so the first note is where
|
||||
// the locator resolves it before clicking.
|
||||
await page.waitForFunction(() => editor.getCameraState() === 'idle')
|
||||
|
||||
await firstNote.click()
|
||||
|
||||
await page.waitForTimeout(1000)
|
||||
|
||||
// The first note, not the one Tab created, should be the focused one.
|
||||
await expect(firstNote.locator('div[contenteditable]')).toBeFocused()
|
||||
})
|
||||
|
||||
test('exits edit mode when dragging from text label with blurred input', async ({ page }) => {
|
||||
await page.goto('http://localhost:5420/end-to-end')
|
||||
await page.waitForSelector('.tl-canvas')
|
||||
|
||||
// Create a geo shape with text
|
||||
await page.keyboard.press('r')
|
||||
await page.mouse.click(200, 200)
|
||||
await page.mouse.click(400, 400)
|
||||
|
||||
// Double-click to edit the shape
|
||||
await page.mouse.dblclick(300, 300)
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
// Verify we're in edit mode
|
||||
expect(
|
||||
await page.evaluate(() => !!document.querySelector('.tl-shape [contenteditable]:focus'))
|
||||
).toBe(true)
|
||||
|
||||
// Click near the text and drag (this should blur the input and exit edit mode)
|
||||
await page.mouse.move(300, 300)
|
||||
await page.mouse.down()
|
||||
await page.mouse.move(350, 350)
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
// Verify we've exited edit mode (no focused contenteditable)
|
||||
expect(
|
||||
await page.evaluate(() => !!document.querySelector('.tl-shape [contenteditable]:focus'))
|
||||
).toBe(false)
|
||||
|
||||
await page.mouse.up()
|
||||
})
|
||||
|
||||
test('selects all text when clicking on blurred text input in editing mode', async ({ page }) => {
|
||||
await page.goto('http://localhost:5420/end-to-end')
|
||||
await page.waitForSelector('.tl-canvas')
|
||||
|
||||
// Create geo shape and enter edit mode
|
||||
await page.keyboard.press('r')
|
||||
await page.mouse.click(200, 200)
|
||||
await page.mouse.click(400, 400)
|
||||
await page.mouse.dblclick(300, 300)
|
||||
|
||||
// Wait for contenteditable to be focused before typing
|
||||
await page.waitForFunction(
|
||||
() => document.activeElement === document.querySelector('.tl-shape [contenteditable]')
|
||||
)
|
||||
|
||||
// Type text
|
||||
await page.keyboard.type('test', { delay: 10 })
|
||||
|
||||
// Verify initial text
|
||||
const contenteditable = page.locator('.tl-shape [contenteditable]')
|
||||
await expect(contenteditable).toHaveText('test')
|
||||
|
||||
// Blur input while staying in editing mode by clicking on the text label (but not the input itself)
|
||||
await page.mouse.click(200, 230)
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
await page.waitForFunction(() => document.getSelection()?.toString() === 'test')
|
||||
|
||||
expect(
|
||||
await page.evaluate(() => !!document.querySelector('.tl-shape div[contenteditable]:focus'))
|
||||
).toBe(true)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user