adebb680e5
When we went from overrides-based to component based UI customisation APIs, we didn't do the toolbar because it had some significant extra complexity around overflowing the contents of the menu into the dropdown. This is really hard to do at render-time with react - you can't introspect what a component will return to move some of it into an overflow. Instead, this diff runs that logic in a `useLayoutEffect` - we render all the items into both the main toolbar and the overflow menu, then in the effect (or if the rendered components change) we use CSS to remove the items we don't need, check which was last active, etc. Originally, I wasn't really into this approach - but i've actually found it to work super well and be very reliable. ### Change Type - [x] `major` — Breaking change - [ ] `dependencies` — Changes to package dependencies[^1] - [ ] `documentation` — Changes to the documentation only[^2] - [ ] `tests` — Changes to any test code only[^2] - [ ] `internal` — Any other changes that don't affect the published package[^2] - [ ] I don't know [^1]: publishes a `patch` release, for devDependencies use `internal` [^2]: will not publish a new version ### Test Plan 1. Test the toolbar at many different sizes with many different 'active tools' --------- Co-authored-by: Steve Ruiz <steveruizok@gmail.com>
178 lines
5 KiB
TypeScript
178 lines
5 KiB
TypeScript
import test, { expect } from '@playwright/test'
|
|
import { Editor } from 'tldraw'
|
|
|
|
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-checked="true"]')).toBe(
|
|
null
|
|
)
|
|
expect(await EditorB.$('.tlui-button[data-testid="tools.draw"][aria-checked="true"]')).toBe(
|
|
null
|
|
)
|
|
|
|
await page.keyboard.press('d')
|
|
|
|
expect(await EditorA.$('.tlui-button[data-testid="tools.draw"][aria-checked="true"]')).not.toBe(
|
|
null
|
|
)
|
|
expect(await EditorB.$('.tlui-button[data-testid="tools.draw"][aria-checked="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-checked="true"]')).not.toBe(
|
|
null
|
|
)
|
|
expect(await EditorB.$('.tlui-button[data-testid="tools.draw"][aria-checked="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-checked="true"]')).toBe(
|
|
null
|
|
)
|
|
|
|
await drawButton?.click()
|
|
|
|
// draw button should be selected now
|
|
expect(await EditorA.$('.tlui-button[data-testid="tools.draw"][aria-checked="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-checked="true"]')).toBe(
|
|
null
|
|
)
|
|
})
|
|
})
|