Fix text-wrapping on Safari (#1980)

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">
This commit is contained in:
Taha 2023-10-02 12:30:53 +01:00 committed by GitHub
parent da33179a31
commit f73bf9a7fe
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 29 additions and 20 deletions

View file

@ -7,7 +7,7 @@ export function sleep(ms: number) {
} }
const measureTextOptions = { const measureTextOptions = {
width: 'fit-content', width: null,
fontFamily: 'var(--tl-font-draw)', fontFamily: 'var(--tl-font-draw)',
fontSize: 24, fontSize: 24,
lineHeight: 1.35, lineHeight: 1.35,

View file

@ -63,7 +63,12 @@ export class TextManager {
fontFamily: string fontFamily: string
fontSize: number fontSize: number
lineHeight: number lineHeight: number
width: string /**
* When width is a number, the text will be wrapped to that width. When
* width is null, the text will be measured without wrapping, but explicit
* line breaks and space are preserved.
*/
width: null | number
minWidth?: string minWidth?: string
maxWidth: string maxWidth: string
padding: string padding: string
@ -77,13 +82,18 @@ export class TextManager {
elm.style.setProperty('font-weight', opts.fontWeight) elm.style.setProperty('font-weight', opts.fontWeight)
elm.style.setProperty('font-size', opts.fontSize + 'px') elm.style.setProperty('font-size', opts.fontSize + 'px')
elm.style.setProperty('line-height', opts.lineHeight * opts.fontSize + 'px') elm.style.setProperty('line-height', opts.lineHeight * opts.fontSize + 'px')
elm.style.setProperty('width', opts.width) if (opts.width === null) {
elm.style.setProperty('white-space', 'pre')
elm.style.setProperty('width', 'fit-content')
} else {
elm.style.setProperty('width', opts.width + 'px')
elm.style.setProperty('white-space', 'pre-wrap')
}
elm.style.setProperty('min-width', opts.minWidth ?? null) elm.style.setProperty('min-width', opts.minWidth ?? null)
elm.style.setProperty('max-width', opts.maxWidth) elm.style.setProperty('max-width', opts.maxWidth)
elm.style.setProperty('padding', opts.padding) elm.style.setProperty('padding', opts.padding)
elm.textContent = normalizeTextForDom(textToMeasure) elm.textContent = normalizeTextForDom(textToMeasure)
const rect = elm.getBoundingClientRect() const rect = elm.getBoundingClientRect()
return { return {

View file

@ -289,7 +289,7 @@ export function registerDefaultExternalContentHandlers(
...TEXT_PROPS, ...TEXT_PROPS,
fontFamily: FONT_FAMILIES[defaultProps.font], fontFamily: FONT_FAMILIES[defaultProps.font],
fontSize: FONT_SIZES[defaultProps.size], fontSize: FONT_SIZES[defaultProps.size],
width: 'fit-content', width: null,
}) })
const minWidth = Math.min( const minWidth = Math.min(
@ -302,7 +302,7 @@ export function registerDefaultExternalContentHandlers(
...TEXT_PROPS, ...TEXT_PROPS,
fontFamily: FONT_FAMILIES[defaultProps.font], fontFamily: FONT_FAMILIES[defaultProps.font],
fontSize: FONT_SIZES[defaultProps.size], fontSize: FONT_SIZES[defaultProps.size],
width: minWidth + 'px', width: minWidth,
}) })
w = shrunkSize.w w = shrunkSize.w
h = shrunkSize.h h = shrunkSize.h

View file

@ -112,7 +112,7 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
...TEXT_PROPS, ...TEXT_PROPS,
fontFamily: FONT_FAMILIES[shape.props.font], fontFamily: FONT_FAMILIES[shape.props.font],
fontSize: ARROW_LABEL_FONT_SIZES[shape.props.size], fontSize: ARROW_LABEL_FONT_SIZES[shape.props.size],
width: 'fit-content', width: null,
}) })
let width = w let width = w
@ -127,7 +127,7 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
...TEXT_PROPS, ...TEXT_PROPS,
fontFamily: FONT_FAMILIES[shape.props.font], fontFamily: FONT_FAMILIES[shape.props.font],
fontSize: ARROW_LABEL_FONT_SIZES[shape.props.size], fontSize: ARROW_LABEL_FONT_SIZES[shape.props.size],
width: width + 'px', width: width,
} }
) )
@ -144,7 +144,7 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
...TEXT_PROPS, ...TEXT_PROPS,
fontFamily: FONT_FAMILIES[shape.props.font], fontFamily: FONT_FAMILIES[shape.props.font],
fontSize: ARROW_LABEL_FONT_SIZES[shape.props.size], fontSize: ARROW_LABEL_FONT_SIZES[shape.props.size],
width: width + 'px', width: width,
} }
) )

View file

@ -1053,7 +1053,7 @@ function getLabelSize(editor: Editor, shape: TLGeoShape) {
...TEXT_PROPS, ...TEXT_PROPS,
fontFamily: FONT_FAMILIES[shape.props.font], fontFamily: FONT_FAMILIES[shape.props.font],
fontSize: LABEL_FONT_SIZES[shape.props.size], fontSize: LABEL_FONT_SIZES[shape.props.size],
width: 'fit-content', width: null,
maxWidth: '100px', maxWidth: '100px',
}) })
@ -1069,7 +1069,7 @@ function getLabelSize(editor: Editor, shape: TLGeoShape) {
...TEXT_PROPS, ...TEXT_PROPS,
fontFamily: FONT_FAMILIES[shape.props.font], fontFamily: FONT_FAMILIES[shape.props.font],
fontSize: LABEL_FONT_SIZES[shape.props.size], fontSize: LABEL_FONT_SIZES[shape.props.size],
width: 'fit-content', width: null,
minWidth: minSize.w + 'px', minWidth: minSize.w + 'px',
maxWidth: maxWidth:
Math.max( Math.max(

View file

@ -194,7 +194,7 @@ function getGrowY(editor: Editor, shape: TLNoteShape, prevGrowY = 0) {
...TEXT_PROPS, ...TEXT_PROPS,
fontFamily: FONT_FAMILIES[shape.props.font], fontFamily: FONT_FAMILIES[shape.props.font],
fontSize: LABEL_FONT_SIZES[shape.props.size], fontSize: LABEL_FONT_SIZES[shape.props.size],
width: NOTE_SIZE - PADDING * 2 + 'px', width: NOTE_SIZE - PADDING * 2,
}) })
const nextHeight = nextTextSize.h + PADDING * 2 const nextHeight = nextTextSize.h + PADDING * 2

View file

@ -373,9 +373,9 @@ function getTextSize(editor: Editor, props: TLTextShape['props']) {
const fontSize = FONT_SIZES[size] const fontSize = FONT_SIZES[size]
const cw = autoSize const cw = autoSize
? 'fit-content' ? null
: // `measureText` floors the number so we need to do the same here to avoid issues. : // `measureText` floors the number so we need to do the same here to avoid issues.
Math.floor(Math.max(minWidth, w)) + 'px' Math.floor(Math.max(minWidth, w))
const result = editor.textMeasure.measureText(text, { const result = editor.textMeasure.measureText(text, {
...TEXT_PROPS, ...TEXT_PROPS,

View file

@ -81,7 +81,7 @@ export class TestEditor extends Editor {
fontFamily: string fontFamily: string
fontSize: number fontSize: number
lineHeight: number lineHeight: number
width: string width: null | number
maxWidth: string maxWidth: string
} }
): Box2dModel => { ): Box2dModel => {
@ -95,18 +95,17 @@ export class TestEditor extends Editor {
return { return {
x: 0, x: 0,
y: 0, y: 0,
w: opts.width.includes('px') ? Math.max(w, +opts.width.replace('px', '')) : w, w: opts.width === null ? w : Math.max(w, opts.width),
h: h:
(opts.width.includes('px') (opts.width === null ? breaks.length : Math.ceil(w % opts.width) + breaks.length) *
? Math.ceil(w % +opts.width.replace('px', '')) + breaks.length opts.fontSize,
: breaks.length) * opts.fontSize,
} }
} }
this.textMeasure.measureTextSpans = (textToMeasure, opts) => { this.textMeasure.measureTextSpans = (textToMeasure, opts) => {
const box = this.textMeasure.measureText(textToMeasure, { const box = this.textMeasure.measureText(textToMeasure, {
...opts, ...opts,
width: `${opts.width}px`, width: opts.width,
padding: `${opts.padding}px`, padding: `${opts.padding}px`,
maxWidth: 'auto', maxWidth: 'auto',
}) })