f73bf9a7fe
Co-authored-by: Alex Alex@dytry.ch closes [#1978](https://github.com/tldraw/tldraw/issues/1978) Text was wrapping on Safari because the measure text div was rendered differently on different browsers. Interestingly, when forcing the text-measure div to be visible and on-screen in Chrome, the same text-wrapping behaviour was apparent. By setting white-space to 'pre' when width hasn't been set by the user, we can ensure that only line breaks the user has inputted are rendered by default on all browsers. ### Change Type - [x] `patch` — Bug fix - [ ] `minor` — New feature - [ ] `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. On Safari 2. Make a new text shape and start typing 3. At a certain point the text starts to wrap without the width having been set ### Release Notes - Fix text wrapping differently on Safari and Chrome/Firefox Before/After <image width="350" src="https://github.com/tldraw/tldraw/assets/98838967/320171b4-61e0-4a41-b8d3-830bd90bea65"> <image width="350" src="https://github.com/tldraw/tldraw/assets/98838967/b42d7156-0ce9-4894-9692-9338dc931b79">
222 lines
6.1 KiB
TypeScript
222 lines
6.1 KiB
TypeScript
import test, { Page, expect } from '@playwright/test'
|
|
import { Box2dModel, Editor } from '@tldraw/tldraw'
|
|
import { setupPage } from '../shared-e2e'
|
|
|
|
export function sleep(ms: number) {
|
|
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
}
|
|
|
|
const measureTextOptions = {
|
|
width: null,
|
|
fontFamily: 'var(--tl-font-draw)',
|
|
fontSize: 24,
|
|
lineHeight: 1.35,
|
|
fontWeight: 'normal',
|
|
fontStyle: 'normal',
|
|
padding: '0px',
|
|
maxWidth: 'auto',
|
|
}
|
|
|
|
const measureTextSpansOptions = {
|
|
width: 100,
|
|
height: 1000,
|
|
overflow: 'wrap' as const,
|
|
padding: 0,
|
|
fontSize: 24,
|
|
fontWeight: 'normal',
|
|
fontFamily: 'var(--tl-font-draw)',
|
|
fontStyle: 'normal',
|
|
lineHeight: 1.35,
|
|
textAlign: 'start' as 'start' | 'middle' | 'end',
|
|
}
|
|
|
|
function formatLines(spans: { box: Box2dModel; text: string }[]) {
|
|
const lines = []
|
|
|
|
let currentLine: string[] | null = null
|
|
let currentLineTop = null
|
|
for (const span of spans) {
|
|
if (currentLineTop !== span.box.y) {
|
|
if (currentLine !== null) {
|
|
lines.push(currentLine)
|
|
}
|
|
currentLine = []
|
|
currentLineTop = span.box.y
|
|
}
|
|
currentLine!.push(span.text)
|
|
}
|
|
|
|
if (currentLine !== null) {
|
|
lines.push(currentLine)
|
|
}
|
|
|
|
return lines
|
|
}
|
|
|
|
declare const editor: Editor
|
|
let page: Page
|
|
|
|
test.describe('text measurement', () => {
|
|
test.beforeAll(async ({ browser }) => {
|
|
page = await browser.newPage()
|
|
await setupPage(page)
|
|
})
|
|
|
|
test('measures text', async () => {
|
|
const { w, h } = await page.evaluate<{ w: number; h: number }, typeof measureTextOptions>(
|
|
async (options) => editor.textMeasure.measureText('testing', options),
|
|
measureTextOptions
|
|
)
|
|
|
|
expect(w).toBeCloseTo(85.828125, 0)
|
|
expect(h).toBeCloseTo(32.3984375, 0)
|
|
})
|
|
|
|
// The text-measurement tests below this point aren't super useful any
|
|
// more. They were added when we had a different approach to text SVG
|
|
// exports (trying to replicate browser decisions with our own code) to
|
|
// what we do now (letting the browser make those decisions then
|
|
// measuring the results).
|
|
//
|
|
// It's hard to write better tests here (e.g. ones where we actually
|
|
// look at the measured values) because the specifics of text layout
|
|
// vary from browser to browser. The ideal thing would be to replace
|
|
// these with visual regression tests for text SVG exports, but we don't
|
|
// have a way of doing visual regression testing right now.
|
|
|
|
test('should get a single text span', async () => {
|
|
const spans = await page.evaluate<
|
|
{ text: string; box: Box2dModel }[],
|
|
typeof measureTextSpansOptions
|
|
>(
|
|
async (options) => editor.textMeasure.measureTextSpans('testing', options),
|
|
measureTextSpansOptions
|
|
)
|
|
|
|
expect(formatLines(spans)).toEqual([['testing']])
|
|
})
|
|
|
|
test('should wrap a word when it has to', async () => {
|
|
const spans = await page.evaluate<
|
|
{ text: string; box: Box2dModel }[],
|
|
typeof measureTextSpansOptions
|
|
>(
|
|
async (options) => editor.textMeasure.measureTextSpans('testing', { ...options, width: 50 }),
|
|
measureTextSpansOptions
|
|
)
|
|
|
|
expect(formatLines(spans)).toEqual([['test'], ['ing']])
|
|
})
|
|
|
|
test('should preserve whitespace at line breaks', async () => {
|
|
const spans = await page.evaluate<
|
|
{ text: string; box: Box2dModel }[],
|
|
typeof measureTextSpansOptions
|
|
>(
|
|
async (options) => editor.textMeasure.measureTextSpans('testing testing', options),
|
|
measureTextSpansOptions
|
|
)
|
|
|
|
expect(formatLines(spans)).toEqual([['testing', ' '], ['testing']])
|
|
})
|
|
|
|
test('should preserve whitespace at the end of wrapped lines', async () => {
|
|
const spans = await page.evaluate<
|
|
{ text: string; box: Box2dModel }[],
|
|
typeof measureTextSpansOptions
|
|
>(
|
|
async (options) => editor.textMeasure.measureTextSpans('testing testing ', options),
|
|
measureTextSpansOptions
|
|
)
|
|
|
|
expect(formatLines(spans)).toEqual([
|
|
['testing', ' '],
|
|
['testing', ' '],
|
|
])
|
|
})
|
|
|
|
test('preserves whitespace at the end of unwrapped lines', async () => {
|
|
const spans = await page.evaluate<
|
|
{ text: string; box: Box2dModel }[],
|
|
typeof measureTextSpansOptions
|
|
>(
|
|
async (options) =>
|
|
editor.textMeasure.measureTextSpans('testing testing ', { ...options, width: 200 }),
|
|
measureTextSpansOptions
|
|
)
|
|
|
|
expect(formatLines(spans)).toEqual([['testing', ' ', 'testing', ' ']])
|
|
})
|
|
|
|
test('preserves whitespace at the start of an unwrapped line', async () => {
|
|
const spans = await page.evaluate<
|
|
{ text: string; box: Box2dModel }[],
|
|
typeof measureTextSpansOptions
|
|
>(
|
|
async (options) =>
|
|
editor.textMeasure.measureTextSpans(' testing testing', { ...options, width: 200 }),
|
|
measureTextSpansOptions
|
|
)
|
|
|
|
expect(formatLines(spans)).toEqual([[' ', 'testing', ' ', 'testing']])
|
|
})
|
|
|
|
test('should place starting whitespace on its own line if it has to', async () => {
|
|
const spans = await page.evaluate<
|
|
{ text: string; box: Box2dModel }[],
|
|
typeof measureTextSpansOptions
|
|
>(
|
|
async (options) => editor.textMeasure.measureTextSpans(' testing testing', options),
|
|
measureTextSpansOptions
|
|
)
|
|
|
|
expect(formatLines(spans)).toEqual([[' '], ['testing', ' '], ['testing']])
|
|
})
|
|
|
|
test('should handle multiline text', async () => {
|
|
const spans = await page.evaluate<
|
|
{ text: string; box: Box2dModel }[],
|
|
typeof measureTextSpansOptions
|
|
>(
|
|
async (options) =>
|
|
editor.textMeasure.measureTextSpans(' test\ning testing \n t', options),
|
|
measureTextSpansOptions
|
|
)
|
|
|
|
expect(formatLines(spans)).toEqual([
|
|
[' ', 'test', '\n'],
|
|
['ing', ' '],
|
|
['testing', ' \n'],
|
|
[' ', 't'],
|
|
])
|
|
})
|
|
|
|
test('should break long strings of text', async () => {
|
|
const spans = await page.evaluate<
|
|
{ text: string; box: Box2dModel }[],
|
|
typeof measureTextSpansOptions
|
|
>(
|
|
async (options) =>
|
|
editor.textMeasure.measureTextSpans('testingtestingtestingtestingtestingtesting', options),
|
|
measureTextSpansOptions
|
|
)
|
|
|
|
expect(formatLines(spans)).toEqual([
|
|
['testingt'],
|
|
['estingte'],
|
|
['stingtes'],
|
|
['tingtest'],
|
|
['ingtesti'],
|
|
['ng'],
|
|
])
|
|
})
|
|
|
|
test('should return an empty array if the text is empty', async () => {
|
|
const spans = await page.evaluate<
|
|
{ text: string; box: Box2dModel }[],
|
|
typeof measureTextSpansOptions
|
|
>(async (options) => editor.textMeasure.measureTextSpans('', options), measureTextSpansOptions)
|
|
|
|
expect(formatLines(spans)).toEqual([])
|
|
})
|
|
})
|