From d76d53db95146c24d35caeca41c2f6d348dbcc06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mime=20=C4=8Cuvalo?= Date: Wed, 27 Mar 2024 09:33:48 +0000 Subject: [PATCH] textfields [1 of 3]: add text into speech bubble; also add rich text example (#3050) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is the first of three textfield changes. This starts with making the speech bubble actually have text. Also, it creates a TipTap example and how that would be wired up. 🎵 this is dangerous, I walk through textfields so watch your head rock 🎵 ### Change Type - [x] `minor` — New feature ### Release Notes - Refactor textfields be composable/swappable. --- .../src/examples/speech-bubble/README.md | 2 +- .../SpeechBubble/SpeechBubbleUtil.tsx | 136 ++++++--- .../speech-bubble/SpeechBubble/helpers.tsx | 25 +- .../examples/speech-bubble/customhandles.css | 6 + packages/editor/editor.css | 114 ++------ packages/tldraw/api-report.md | 36 +++ packages/tldraw/api/api.json | 275 ++++++++++++++++++ packages/tldraw/src/index.ts | 10 + .../src/lib/shapes/arrow/ArrowShapeUtil.tsx | 5 +- .../arrow/components/ArrowTextLabel.tsx | 84 +----- .../src/lib/shapes/geo/GeoShapeUtil.tsx | 5 +- .../src/lib/shapes/note/NoteShapeUtil.tsx | 3 +- .../src/lib/shapes/shared/TextLabel.tsx | 117 +++----- .../shapes/shared/default-shape-constants.ts | 2 + .../src/lib/shapes/shared/useEditableText.ts | 8 +- .../tldraw/src/lib/shapes/text/TextArea.tsx | 53 ++++ .../src/lib/shapes/text/TextShapeUtil.tsx | 81 ++---- 17 files changed, 616 insertions(+), 346 deletions(-) create mode 100644 packages/tldraw/src/lib/shapes/text/TextArea.tsx diff --git a/apps/examples/src/examples/speech-bubble/README.md b/apps/examples/src/examples/speech-bubble/README.md index 1eb61d913..84bc6670b 100644 --- a/apps/examples/src/examples/speech-bubble/README.md +++ b/apps/examples/src/examples/speech-bubble/README.md @@ -2,7 +2,7 @@ title: Speech bubble component: ./CustomShapeWithHandles.tsx category: shapes/tools -priority: 2 +priority: 1 --- A custom shape with handles diff --git a/apps/examples/src/examples/speech-bubble/SpeechBubble/SpeechBubbleUtil.tsx b/apps/examples/src/examples/speech-bubble/SpeechBubble/SpeechBubbleUtil.tsx index c8465b179..cc9bd92b7 100644 --- a/apps/examples/src/examples/speech-bubble/SpeechBubble/SpeechBubbleUtil.tsx +++ b/apps/examples/src/examples/speech-bubble/SpeechBubble/SpeechBubbleUtil.tsx @@ -1,19 +1,24 @@ +import { ShapePropsType } from '@tldraw/tlschema/src/shapes/TLBaseShape' import { DefaultColorStyle, + DefaultFontStyle, + DefaultHorizontalAlignStyle, DefaultSizeStyle, + DefaultVerticalAlignStyle, + FONT_FAMILIES, Geometry2d, + LABEL_FONT_SIZES, Polygon2d, ShapeUtil, T, + TEXT_PROPS, TLBaseShape, - TLDefaultColorStyle, - TLDefaultSizeStyle, TLHandle, TLOnBeforeUpdateHandler, TLOnHandleDragHandler, TLOnResizeHandler, + TextLabel, Vec, - VecModel, ZERO_INDEX_KEY, getDefaultColorTheme, resizeBox, @@ -33,28 +38,28 @@ export const STROKE_SIZES = { // There's a guide at the bottom of this file! // [1] -export type SpeechBubbleShape = TLBaseShape< - 'speech-bubble', - { - w: number - h: number - size: TLDefaultSizeStyle - color: TLDefaultColorStyle - tail: VecModel - } -> + +export const speechBubbleShapeProps = { + w: T.number, + h: T.number, + size: DefaultSizeStyle, + color: DefaultColorStyle, + font: DefaultFontStyle, + align: DefaultHorizontalAlignStyle, + verticalAlign: DefaultVerticalAlignStyle, + growY: T.positiveNumber, + text: T.string, + tail: vecModelValidator, +} + +export type SpeechBubbleShapeProps = ShapePropsType +export type SpeechBubbleShape = TLBaseShape<'speech-bubble', SpeechBubbleShapeProps> export class SpeechBubbleUtil extends ShapeUtil { static override type = 'speech-bubble' as const // [2] - static override props = { - w: T.number, - h: T.number, - size: DefaultSizeStyle, - color: DefaultColorStyle, - tail: vecModelValidator, - } + static override props = speechBubbleShapeProps override isAspectRatioLocked = (_shape: SpeechBubbleShape) => false @@ -62,17 +67,28 @@ export class SpeechBubbleUtil extends ShapeUtil { override canBind = (_shape: SpeechBubbleShape) => true + override canEdit = () => true + // [3] - getDefaultProps(): SpeechBubbleShape['props'] { + getDefaultProps(): SpeechBubbleShapeProps { return { w: 200, h: 130, color: 'black', size: 'm', + font: 'draw', + align: 'middle', + verticalAlign: 'start', + growY: 0, + text: '', tail: { x: 0.5, y: 1.5 }, } } + getHeight(shape: SpeechBubbleShape) { + return shape.props.h + shape.props.growY + } + getGeometry(shape: SpeechBubbleShape): Geometry2d { const speechBubbleGeometry = getSpeechBubbleVertices(shape) const body = new Polygon2d({ @@ -84,7 +100,7 @@ export class SpeechBubbleUtil extends ShapeUtil { // [4] override getHandles(shape: SpeechBubbleShape): TLHandle[] { - const { tail, w, h } = shape.props + const { tail, w } = shape.props return [ { @@ -94,7 +110,7 @@ export class SpeechBubbleUtil extends ShapeUtil { // props.tail coordinates are normalized // but here we need them in shape space x: tail.x * w, - y: tail.y * h, + y: tail.y * this.getHeight(shape), }, ] } @@ -105,29 +121,34 @@ export class SpeechBubbleUtil extends ShapeUtil { props: { tail: { x: handle.x / shape.props.w, - y: handle.y / shape.props.h, + y: handle.y / this.getHeight(shape), }, }, } } + override onBeforeCreate = (next: SpeechBubbleShape) => { + return this.getGrowY(next, next.props.growY) + } + // [5] override onBeforeUpdate: TLOnBeforeUpdateHandler | undefined = ( - _: SpeechBubbleShape, + prev: SpeechBubbleShape, shape: SpeechBubbleShape ) => { - const { w, h, tail } = shape.props + const { w, tail } = shape.props + const fullHeight = this.getHeight(shape) const { segmentsIntersection, insideShape } = getTailIntersectionPoint(shape) - const slantedLength = Math.hypot(w, h) + const slantedLength = Math.hypot(w, fullHeight) const MIN_DISTANCE = slantedLength / 5 const MAX_DISTANCE = slantedLength / 1.5 - const tailInShapeSpace = new Vec(tail.x * w, tail.y * h) + const tailInShapeSpace = new Vec(tail.x * w, tail.y * fullHeight) const distanceToIntersection = tailInShapeSpace.dist(segmentsIntersection) - const center = new Vec(w / 2, h / 2) + const center = new Vec(w / 2, fullHeight / 2) const tailDirection = Vec.Sub(tailInShapeSpace, center).uni() let newPoint = tailInShapeSpace @@ -144,12 +165,17 @@ export class SpeechBubbleUtil extends ShapeUtil { const next = structuredClone(shape) next.props.tail.x = newPoint.x / w - next.props.tail.y = newPoint.y / h + next.props.tail.y = newPoint.y / fullHeight - return next + return this.getGrowY(next, prev.props.growY) } component(shape: SpeechBubbleShape) { + const { + id, + type, + props: { color, font, size, align, text }, + } = shape const theme = getDefaultColorTheme({ isDarkMode: this.editor.user.getIsDarkMode(), }) @@ -161,11 +187,24 @@ export class SpeechBubbleUtil extends ShapeUtil { + + ) } @@ -185,6 +224,37 @@ export class SpeechBubbleUtil extends ShapeUtil { next.props.h = resized.props.h return next } + + getGrowY(shape: SpeechBubbleShape, prevGrowY = 0) { + const PADDING = 17 + + const nextTextSize = this.editor.textMeasure.measureText(shape.props.text, { + ...TEXT_PROPS, + fontFamily: FONT_FAMILIES[shape.props.font], + fontSize: LABEL_FONT_SIZES[shape.props.size], + maxWidth: shape.props.w - PADDING * 2, + }) + + const nextHeight = nextTextSize.h + PADDING * 2 + + let growY = 0 + + if (nextHeight > shape.props.h) { + growY = nextHeight - shape.props.h + } else { + if (prevGrowY) { + growY = 0 + } + } + + return { + ...shape, + props: { + ...shape.props, + growY, + }, + } + } } /* diff --git a/apps/examples/src/examples/speech-bubble/SpeechBubble/helpers.tsx b/apps/examples/src/examples/speech-bubble/SpeechBubble/helpers.tsx index 82953612a..8f609612d 100644 --- a/apps/examples/src/examples/speech-bubble/SpeechBubble/helpers.tsx +++ b/apps/examples/src/examples/speech-bubble/SpeechBubble/helpers.tsx @@ -2,14 +2,20 @@ import { Vec, VecLike, lerp, pointInPolygon } from 'tldraw' import { SpeechBubbleShape } from './SpeechBubbleUtil' export const getSpeechBubbleVertices = (shape: SpeechBubbleShape): Vec[] => { - const { w, h, tail } = shape.props + const { w, tail } = shape.props - const tailInShapeSpace = new Vec(tail.x * w, tail.y * h) + const fullHeight = shape.props.h + shape.props.growY + const tailInShapeSpace = new Vec(tail.x * w, tail.y * fullHeight) - const [tl, tr, br, bl] = [new Vec(0, 0), new Vec(w, 0), new Vec(w, h), new Vec(0, h)] + const [tl, tr, br, bl] = [ + new Vec(0, 0), + new Vec(w, 0), + new Vec(w, fullHeight), + new Vec(0, fullHeight), + ] const offsetH = w / 10 - const offsetV = h / 10 + const offsetV = fullHeight / 10 const { adjustedIntersection, intersectionSegmentIndex } = getTailIntersectionPoint(shape) @@ -73,11 +79,12 @@ export const getSpeechBubbleVertices = (shape: SpeechBubbleShape): Vec[] => { } export function getTailIntersectionPoint(shape: SpeechBubbleShape) { - const { w, h, tail } = shape.props - const tailInShapeSpace = new Vec(tail.x * w, tail.y * h) + const { w, tail } = shape.props + const fullHeight = shape.props.h + shape.props.growY + const tailInShapeSpace = new Vec(tail.x * w, tail.y * fullHeight) - const center = new Vec(w / 2, h / 2) - const corners = [new Vec(0, 0), new Vec(w, 0), new Vec(w, h), new Vec(0, h)] + const center = new Vec(w / 2, fullHeight / 2) + const corners = [new Vec(0, 0), new Vec(w, 0), new Vec(w, fullHeight), new Vec(0, fullHeight)] const segments = [ [corners[0], corners[1]], [corners[1], corners[2]], @@ -132,7 +139,7 @@ export function getTailIntersectionPoint(shape: SpeechBubbleShape) { const squared = mapRange(-1, 1, 0, totalDistance, squaredRelative) // -1 to 1 -> absolute //keep it away from the edges - const offset = (segments.indexOf(intersectionSegment) % 2 === 0 ? w / 10 : h / 10) * 3 + const offset = (segments.indexOf(intersectionSegment) % 2 === 0 ? w / 10 : fullHeight / 10) * 3 const constrained = mapRange(0, totalDistance, offset, totalDistance - offset, distance) // combine the two diff --git a/apps/examples/src/examples/speech-bubble/customhandles.css b/apps/examples/src/examples/speech-bubble/customhandles.css index 8dae68671..f7afe0410 100644 --- a/apps/examples/src/examples/speech-bubble/customhandles.css +++ b/apps/examples/src/examples/speech-bubble/customhandles.css @@ -2,3 +2,9 @@ .tl-user-handles { z-index: 101; } + +/* The text label doesn't normally deal with text that goes sideways, + * so this accounts for that */ +.tl-shape[data-shape-type='speech-bubble'] .tl-text-label { + justify-content: flex-start !important; +} diff --git a/packages/editor/editor.css b/packages/editor/editor.css index d2626a3a6..65e64a7d7 100644 --- a/packages/editor/editor.css +++ b/packages/editor/editor.css @@ -682,7 +682,7 @@ input, /* ------------------- Text Shape ------------------- */ -.tl-text-shape__wrapper { +.tl-text-shape-label { position: relative; font-weight: normal; min-width: 1px; @@ -698,35 +698,38 @@ input, text-shadow: var(--tl-text-outline); } -.tl-text-shape__wrapper[data-align='start'] { - text-align: left; -} - -.tl-text-shape__wrapper[data-align='middle'] { - text-align: center; -} - -.tl-text-shape__wrapper[data-align='end'] { - text-align: right; -} - -.tl-text-shape__wrapper[data-font='draw'] { +.tl-text-wrapper[data-font='draw'] { font-family: var(--tl-font-draw); } -.tl-text-shape__wrapper[data-font='sans'] { +.tl-text-wrapper[data-font='sans'] { font-family: var(--tl-font-sans); } -.tl-text-shape__wrapper[data-font='serif'] { +.tl-text-wrapper[data-font='serif'] { font-family: var(--tl-font-serif); } -.tl-text-shape__wrapper[data-font='mono'] { +.tl-text-wrapper[data-font='mono'] { font-family: var(--tl-font-mono); } -.tl-text-shape__wrapper[data-isediting='true'] .tl-text-content { +.tl-text-wrapper[data-align='start'], +.tl-text-wrapper[data-align='start-legacy'] { + text-align: left; +} + +.tl-text-wrapper[data-align='middle'], +.tl-text-wrapper[data-align='middle-legacy'] { + text-align: center; +} + +.tl-text-wrapper[data-align='end'], +.tl-text-wrapper[data-align='end-legacy'] { + text-align: right; +} + +.tl-text-wrapper[data-isediting='true'] .tl-text-content { opacity: 0; } @@ -995,10 +998,6 @@ input, z-index: 10; } -.tl-text-label[data-isediting='true'] .tl-text-content { - opacity: 0; -} - .tl-text-label[data-hastext='false'][data-isediting='false'] > .tl-text-label__inner { width: 40px; height: 40px; @@ -1053,21 +1052,6 @@ input, opacity: 0; } -.tl-text-label[data-align='start'], -.tl-text-label[data-align='start-legacy'] { - text-align: left; -} - -.tl-text-label[data-align='middle'], -.tl-text-label[data-align='middle-legacy'] { - text-align: center; -} - -.tl-text-label[data-align='end'], -.tl-text-label[data-align='end-legacy'] { - text-align: right; -} - .tl-arrow-hint { stroke: var(--color-text-1); fill: none; @@ -1075,26 +1059,6 @@ input, overflow: visible; } -.tl-arrow-label[data-font='draw'], -.tl-text-label[data-font='draw'] { - font-family: var(--tl-font-draw); -} - -.tl-arrow-label[data-font='sans'], -.tl-text-label[data-font='sans'] { - font-family: var(--tl-font-sans); -} - -.tl-arrow-label[data-font='serif'], -.tl-text-label[data-font='serif'] { - font-family: var(--tl-font-serif); -} - -.tl-arrow-label[data-font='mono'], -.tl-text-label[data-font='mono'] { - font-family: var(--tl-font-mono); -} - /* ------------------- Arrow Shape ------------------ */ .tl-arrow-label { @@ -1107,6 +1071,7 @@ input, display: flex; justify-content: center; align-items: center; + text-align: center; color: var(--color-text); text-shadow: var(--tl-text-outline); } @@ -1131,40 +1096,7 @@ input, align-items: center; } -.tl-arrow-label p, -.tl-arrow-label textarea { - margin: 0px; - padding: 0px; - border: 0px; - color: inherit; - caret-color: var(--color-text); - background: none; - border-image: none; - font-size: inherit; - font-family: inherit; - font-weight: inherit; - line-height: inherit; - font-variant: inherit; - font-style: inherit; - text-align: inherit; - letter-spacing: inherit; - text-shadow: inherit; - outline: none; - white-space: pre-wrap; - word-wrap: break-word; - overflow-wrap: break-word; - pointer-events: all; - text-rendering: auto; - text-transform: none; - text-indent: 0px; - display: inline-block; - appearance: auto; - column-count: initial !important; - writing-mode: horizontal-tb !important; - word-spacing: 0px; -} - -.tl-arrow-label p { +.tl-arrow-label .tl-arrow { position: relative; height: max-content; z-index: 2; diff --git a/packages/tldraw/api-report.md b/packages/tldraw/api-report.md index 60730cd6d..2aaeb4125 100644 --- a/packages/tldraw/api-report.md +++ b/packages/tldraw/api-report.md @@ -62,8 +62,13 @@ import { TLBookmarkShape } from '@tldraw/editor'; import { TLCancelEvent } from '@tldraw/editor'; import { TLClickEvent } from '@tldraw/editor'; import { TLClickEventInfo } from '@tldraw/editor'; +import { TLDefaultColorStyle } from '@tldraw/editor'; import { TLDefaultColorTheme } from '@tldraw/editor'; +import { TLDefaultFillStyle } from '@tldraw/editor'; +import { TLDefaultFontStyle } from '@tldraw/editor'; +import { TLDefaultHorizontalAlignStyle } from '@tldraw/editor'; import { TLDefaultSizeStyle } from '@tldraw/editor'; +import { TLDefaultVerticalAlignStyle } from '@tldraw/editor'; import { TldrawEditorBaseProps } from '@tldraw/editor'; import { TLDrawShape } from '@tldraw/editor'; import { TLDrawShapeSegment } from '@tldraw/editor'; @@ -613,6 +618,9 @@ export function fitFrameToContent(editor: Editor, id: TLShapeId, opts?: { // @public (undocumented) export function FitFrameToContentMenuItem(): JSX_2.Element | null; +// @public (undocumented) +export const FONT_FAMILIES: Record; + // @public (undocumented) export class FrameShapeTool extends BaseBoxShapeTool { // (undocumented) @@ -966,6 +974,9 @@ export function isGifAnimated(file: Blob): Promise; // @public (undocumented) export function KeyboardShortcutsMenuItem(): JSX_2.Element | null; +// @public (undocumented) +export const LABEL_FONT_SIZES: Record; + // @public (undocumented) export function LanguageMenu(): JSX_2.Element; @@ -1279,6 +1290,18 @@ export function StackMenuItems(): JSX_2.Element; // @public (undocumented) export function StarToolbarItem(): JSX_2.Element; +// @public (undocumented) +export const TEXT_PROPS: { + lineHeight: number; + fontWeight: string; + fontVariant: string; + fontStyle: string; + padding: string; +}; + +// @public (undocumented) +export const TextLabel: React_2.NamedExoticComponent; + // @public (undocumented) export class TextShapeTool extends StateNode { // (undocumented) @@ -2475,6 +2498,19 @@ export function useDefaultHelpers(): { // @public (undocumented) export function useDialogs(): TLUiDialogsContextType; +// @public (undocumented) +export function useEditableText(id: TLShapeId, type: string, text: string): { + rInput: React_2.RefObject; + isEditing: boolean; + handleFocus: () => void; + handleBlur: () => void; + handleKeyDown: (e: React_2.KeyboardEvent) => void; + handleChange: (e: React_2.ChangeEvent) => void; + handleInputPointerDown: (e: React_2.PointerEvent) => void; + handleDoubleClick: (e: any) => any; + isEmpty: boolean; +}; + // @public (undocumented) export function useExportAs(): (ids: TLShapeId[], format: TLExportType | undefined, name: string | undefined) => void; diff --git a/packages/tldraw/api/api.json b/packages/tldraw/api/api.json index 332f1fa42..5cf599bcf 100644 --- a/packages/tldraw/api/api.json +++ b/packages/tldraw/api/api.json @@ -6887,6 +6887,43 @@ "parameters": [], "name": "FitFrameToContentMenuItem" }, + { + "kind": "Variable", + "canonicalReference": "tldraw!FONT_FAMILIES:var", + "docComment": "/**\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "FONT_FAMILIES: " + }, + { + "kind": "Reference", + "text": "Record", + "canonicalReference": "!Record:type" + }, + { + "kind": "Content", + "text": "<" + }, + { + "kind": "Reference", + "text": "TLDefaultFontStyle", + "canonicalReference": "@tldraw/tlschema!TLDefaultFontStyle:type" + }, + { + "kind": "Content", + "text": ", string>" + } + ], + "fileUrlPath": "packages/tldraw/src/lib/shapes/shared/default-shape-constants.ts", + "isReadonly": true, + "releaseTag": "Public", + "name": "FONT_FAMILIES", + "variableTypeTokenRange": { + "startIndex": 1, + "endIndex": 5 + } + }, { "kind": "Class", "canonicalReference": "tldraw!FrameShapeTool:class", @@ -11244,6 +11281,43 @@ "parameters": [], "name": "KeyboardShortcutsMenuItem" }, + { + "kind": "Variable", + "canonicalReference": "tldraw!LABEL_FONT_SIZES:var", + "docComment": "/**\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "LABEL_FONT_SIZES: " + }, + { + "kind": "Reference", + "text": "Record", + "canonicalReference": "!Record:type" + }, + { + "kind": "Content", + "text": "<" + }, + { + "kind": "Reference", + "text": "TLDefaultSizeStyle", + "canonicalReference": "@tldraw/tlschema!TLDefaultSizeStyle:type" + }, + { + "kind": "Content", + "text": ", number>" + } + ], + "fileUrlPath": "packages/tldraw/src/lib/shapes/shared/default-shape-constants.ts", + "isReadonly": true, + "releaseTag": "Public", + "name": "LABEL_FONT_SIZES", + "variableTypeTokenRange": { + "startIndex": 1, + "endIndex": 5 + } + }, { "kind": "Function", "canonicalReference": "tldraw!LanguageMenu:function(1)", @@ -14997,6 +15071,66 @@ "parameters": [], "name": "StarToolbarItem" }, + { + "kind": "Variable", + "canonicalReference": "tldraw!TEXT_PROPS:var", + "docComment": "/**\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "TEXT_PROPS: " + }, + { + "kind": "Content", + "text": "{\n lineHeight: number;\n fontWeight: string;\n fontVariant: string;\n fontStyle: string;\n padding: string;\n}" + } + ], + "fileUrlPath": "packages/tldraw/src/lib/shapes/shared/default-shape-constants.ts", + "isReadonly": true, + "releaseTag": "Public", + "name": "TEXT_PROPS", + "variableTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + } + }, + { + "kind": "Variable", + "canonicalReference": "tldraw!TextLabel:var", + "docComment": "/**\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "TextLabel: " + }, + { + "kind": "Reference", + "text": "React.NamedExoticComponent", + "canonicalReference": "@types/react!React.NamedExoticComponent:interface" + }, + { + "kind": "Content", + "text": "<" + }, + { + "kind": "Reference", + "text": "TextLabelProps", + "canonicalReference": "tldraw!~TextLabelProps:type" + }, + { + "kind": "Content", + "text": ">" + } + ], + "fileUrlPath": "packages/tldraw/src/lib/shapes/shared/TextLabel.tsx", + "isReadonly": true, + "releaseTag": "Public", + "name": "TextLabel", + "variableTypeTokenRange": { + "startIndex": 1, + "endIndex": 5 + } + }, { "kind": "Class", "canonicalReference": "tldraw!TextShapeTool:class", @@ -27289,6 +27423,147 @@ "parameters": [], "name": "useDialogs" }, + { + "kind": "Function", + "canonicalReference": "tldraw!useEditableText:function(1)", + "docComment": "/**\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "export declare function useEditableText(id: " + }, + { + "kind": "Reference", + "text": "TLShapeId", + "canonicalReference": "@tldraw/tlschema!TLShapeId:type" + }, + { + "kind": "Content", + "text": ", type: " + }, + { + "kind": "Content", + "text": "string" + }, + { + "kind": "Content", + "text": ", text: " + }, + { + "kind": "Content", + "text": "string" + }, + { + "kind": "Content", + "text": "): " + }, + { + "kind": "Content", + "text": "{\n rInput: " + }, + { + "kind": "Reference", + "text": "React.RefObject", + "canonicalReference": "@types/react!React.RefObject:interface" + }, + { + "kind": "Content", + "text": "<" + }, + { + "kind": "Reference", + "text": "HTMLTextAreaElement", + "canonicalReference": "!HTMLTextAreaElement:interface" + }, + { + "kind": "Content", + "text": ">;\n isEditing: boolean;\n handleFocus: () => void;\n handleBlur: () => void;\n handleKeyDown: (e: " + }, + { + "kind": "Reference", + "text": "React.KeyboardEvent", + "canonicalReference": "@types/react!React.KeyboardEvent:interface" + }, + { + "kind": "Content", + "text": "<" + }, + { + "kind": "Reference", + "text": "HTMLTextAreaElement", + "canonicalReference": "!HTMLTextAreaElement:interface" + }, + { + "kind": "Content", + "text": ">) => void;\n handleChange: (e: " + }, + { + "kind": "Reference", + "text": "React.ChangeEvent", + "canonicalReference": "@types/react!React.ChangeEvent:interface" + }, + { + "kind": "Content", + "text": "<" + }, + { + "kind": "Reference", + "text": "HTMLTextAreaElement", + "canonicalReference": "!HTMLTextAreaElement:interface" + }, + { + "kind": "Content", + "text": ">) => void;\n handleInputPointerDown: (e: " + }, + { + "kind": "Reference", + "text": "React.PointerEvent", + "canonicalReference": "@types/react!React.PointerEvent:interface" + }, + { + "kind": "Content", + "text": ") => void;\n handleDoubleClick: (e: any) => any;\n isEmpty: boolean;\n}" + }, + { + "kind": "Content", + "text": ";" + } + ], + "fileUrlPath": "packages/tldraw/src/lib/shapes/shared/useEditableText.ts", + "returnTypeTokenRange": { + "startIndex": 7, + "endIndex": 22 + }, + "releaseTag": "Public", + "overloadIndex": 1, + "parameters": [ + { + "parameterName": "id", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isOptional": false + }, + { + "parameterName": "type", + "parameterTypeTokenRange": { + "startIndex": 3, + "endIndex": 4 + }, + "isOptional": false + }, + { + "parameterName": "text", + "parameterTypeTokenRange": { + "startIndex": 5, + "endIndex": 6 + }, + "isOptional": false + } + ], + "name": "useEditableText" + }, { "kind": "Function", "canonicalReference": "tldraw!useExportAs:function(1)", diff --git a/packages/tldraw/src/index.ts b/packages/tldraw/src/index.ts index 4154a07f7..eb954b308 100644 --- a/packages/tldraw/src/index.ts +++ b/packages/tldraw/src/index.ts @@ -33,6 +33,7 @@ export { LineShapeTool } from './lib/shapes/line/LineShapeTool' export { LineShapeUtil } from './lib/shapes/line/LineShapeUtil' export { NoteShapeTool } from './lib/shapes/note/NoteShapeTool' export { NoteShapeUtil } from './lib/shapes/note/NoteShapeUtil' +export { TextLabel } from './lib/shapes/shared/TextLabel' export { TextShapeTool } from './lib/shapes/text/TextShapeTool' export { TextShapeUtil } from './lib/shapes/text/TextShapeUtil' export { VideoShapeUtil } from './lib/shapes/video/VideoShapeUtil' @@ -42,6 +43,7 @@ export { LaserTool } from './lib/tools/LaserTool/LaserTool' export { SelectTool } from './lib/tools/SelectTool/SelectTool' export { ZoomTool } from './lib/tools/ZoomTool/ZoomTool' // UI +export { useEditableText } from './lib/shapes/shared/useEditableText' export { TldrawUi, type TldrawUiBaseProps, type TldrawUiProps } from './lib/ui/TldrawUi' export { setDefaultUiAssetUrls, type TLUiAssetUrlOverrides } from './lib/ui/assetUrls' export { OfflineIndicator } from './lib/ui/components/OfflineIndicator/OfflineIndicator' @@ -422,3 +424,11 @@ export { TldrawUiMenuSubmenu, type TLUiMenuSubmenuProps, } from './lib/ui/components/primitives/menus/TldrawUiMenuSubmenu' + +/* ----------------- Constants ---------------- */ + +export { + FONT_FAMILIES, + LABEL_FONT_SIZES, + TEXT_PROPS, +} from './lib/shapes/shared/default-shape-constants' diff --git a/packages/tldraw/src/lib/shapes/arrow/ArrowShapeUtil.tsx b/packages/tldraw/src/lib/shapes/arrow/ArrowShapeUtil.tsx index cbe043bad..09a616bfc 100644 --- a/packages/tldraw/src/lib/shapes/arrow/ArrowShapeUtil.tsx +++ b/packages/tldraw/src/lib/shapes/arrow/ArrowShapeUtil.tsx @@ -516,9 +516,6 @@ export class ArrowShapeUtil extends ShapeUtil { } component(shape: TLArrowShape) { - // Not a class component, but eslint can't tell that :( - // eslint-disable-next-line react-hooks/rules-of-hooks - const theme = useDefaultColorTheme() const onlySelectedShape = this.editor.getOnlySelectedShape() const shouldDisplayHandles = this.editor.isInAny( @@ -549,7 +546,7 @@ export class ArrowShapeUtil extends ShapeUtil { size={shape.props.size} position={labelPosition.box.center} width={labelPosition.box.w} - labelColor={theme[shape.props.labelColor].solid} + labelColor={shape.props.labelColor} /> ) diff --git a/packages/tldraw/src/lib/shapes/arrow/components/ArrowTextLabel.tsx b/packages/tldraw/src/lib/shapes/arrow/components/ArrowTextLabel.tsx index b7eb17e36..f2cdc12b4 100644 --- a/packages/tldraw/src/lib/shapes/arrow/components/ArrowTextLabel.tsx +++ b/packages/tldraw/src/lib/shapes/arrow/components/ArrowTextLabel.tsx @@ -1,8 +1,7 @@ -import { TLArrowShape, TLShapeId, VecLike, stopEventPropagation } from '@tldraw/editor' +import { TLArrowShape, TLDefaultColorStyle, TLShapeId, VecLike } from '@tldraw/editor' import * as React from 'react' -import { TextHelpers } from '../../shared/TextHelpers' +import { TextLabel } from '../../shared/TextLabel' import { ARROW_LABEL_FONT_SIZES, TEXT_PROPS } from '../../shared/default-shape-constants' -import { useEditableText } from '../../shared/useEditableText' export const ArrowTextLabel = React.memo(function ArrowTextLabel({ id, @@ -12,77 +11,26 @@ export const ArrowTextLabel = React.memo(function ArrowTextLabel({ position, width, labelColor, -}: { id: TLShapeId; position: VecLike; width?: number; labelColor: string } & Pick< +}: { id: TLShapeId; position: VecLike; width?: number; labelColor: TLDefaultColorStyle } & Pick< TLArrowShape['props'], 'text' | 'size' | 'font' >) { - const { - rInput, - isEditing, - handleFocus, - handleBlur, - handleKeyDown, - handleChange, - isEmpty, - handleInputPointerDown, - handleDoubleClick, - } = useEditableText(id, 'arrow', text) - - const finalText = TextHelpers.normalizeTextForDom(text) - const hasText = finalText.trim().length > 0 - - if (!isEditing && !hasText) { - return null - } - return ( -
-
-

- {text ? TextHelpers.normalizeTextForDom(text) : ' '} -

- {isEditing && ( - // Consider replacing with content-editable -