Vertical text alignment for geo shapes (#1414)

Vertical text alignment for geo shapes.

### Change Type

- [x] `minor` — New Feature

### Test Plan

1. Add a step-by-step description of how to test your PR here.
2.

- [ ] Unit Tests
- [ ] Webdriver tests

### Release Notes

- This adds vertical text alignment property to geo shapes.

---------

Co-authored-by: Steve Ruiz <steveruizok@gmail.com>
This commit is contained in:
Mitja Bezenšek 2023-05-19 12:23:43 +02:00 committed by GitHub
parent ddca01918f
commit f59bfe01b1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 254 additions and 21 deletions

View file

@ -0,0 +1,7 @@
<svg width="30" height="31" viewBox="0 0 30 31" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="15" y="19.2969" width="2" height="8" rx="1" transform="rotate(45 15 19.2969)" fill="black"/>
<rect x="13.5858" y="20.7111" width="2" height="8" rx="1" transform="rotate(-45 13.5858 20.7111)" fill="black"/>
<rect x="15" y="11.1179" width="2" height="8" rx="1" transform="rotate(-135 15 11.1179)" fill="black"/>
<rect x="16.4142" y="9.70374" width="2" height="8" rx="1" transform="rotate(135 16.4142 9.70374)" fill="black"/>
<rect x="4" y="16.2069" width="2" height="22" rx="1" transform="rotate(-90 4 16.2069)" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 637 B

View file

@ -0,0 +1,5 @@
<svg width="30" height="31" viewBox="0 0 30 31" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="26" y="19.7069" width="2" height="22" rx="1" transform="rotate(90 26 19.7069)" fill="black"/>
<rect x="15" y="16.6169" width="2" height="8" rx="1" transform="rotate(-135 15 16.6169)" fill="black"/>
<rect x="16.4142" y="15.2028" width="2" height="8" rx="1" transform="rotate(135 16.4142 15.2028)" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 423 B

View file

@ -0,0 +1,5 @@
<svg width="30" height="31" viewBox="0 0 30 31" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="4" y="10.7069" width="2" height="22" rx="1" transform="rotate(-90 4 10.7069)" fill="black"/>
<rect x="15" y="13.7986" width="2" height="8" rx="1" transform="rotate(45 15 13.7986)" fill="black"/>
<rect x="13.5858" y="15.2128" width="2" height="8" rx="1" transform="rotate(-45 13.5858 15.2128)" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 420 B

View file

@ -272,6 +272,7 @@
"shortcuts-dialog.view": "View", "shortcuts-dialog.view": "View",
"style-panel.title": "Styles", "style-panel.title": "Styles",
"style-panel.align": "Align", "style-panel.align": "Align",
"style-panel.vertical-align": "Vertical align",
"style-panel.position": "Position", "style-panel.position": "Position",
"style-panel.arrowheads": "Arrowheads", "style-panel.arrowheads": "Arrowheads",
"style-panel.arrowhead-start": "Start", "style-panel.arrowhead-start": "Start",

View file

@ -165,6 +165,9 @@ export function getAssetUrlsByImport(opts?: AssetUrlOptions): {
ungroup: string ungroup: string
'unlock-small': string 'unlock-small': string
unlock: string unlock: string
'vertical-align-center': string
'vertical-align-end': string
'vertical-align-start': string
visible: string visible: string
'warning-triangle': string 'warning-triangle': string
'zoom-in': string 'zoom-in': string

View file

@ -176,6 +176,9 @@ import iconsUndo from './icons/icon/undo.svg'
import iconsUngroup from './icons/icon/ungroup.svg' import iconsUngroup from './icons/icon/ungroup.svg'
import iconsUnlockSmall from './icons/icon/unlock-small.svg' import iconsUnlockSmall from './icons/icon/unlock-small.svg'
import iconsUnlock from './icons/icon/unlock.svg' import iconsUnlock from './icons/icon/unlock.svg'
import iconsVerticalAlignCenter from './icons/icon/vertical-align-center.svg'
import iconsVerticalAlignEnd from './icons/icon/vertical-align-end.svg'
import iconsVerticalAlignStart from './icons/icon/vertical-align-start.svg'
import iconsVisible from './icons/icon/visible.svg' import iconsVisible from './icons/icon/visible.svg'
import iconsWarningTriangle from './icons/icon/warning-triangle.svg' import iconsWarningTriangle from './icons/icon/warning-triangle.svg'
import iconsZoomIn from './icons/icon/zoom-in.svg' import iconsZoomIn from './icons/icon/zoom-in.svg'
@ -403,6 +406,9 @@ export function getAssetUrlsByImport(opts) {
ungroup: formatAssetUrl(iconsUngroup, opts), ungroup: formatAssetUrl(iconsUngroup, opts),
'unlock-small': formatAssetUrl(iconsUnlockSmall, opts), 'unlock-small': formatAssetUrl(iconsUnlockSmall, opts),
unlock: formatAssetUrl(iconsUnlock, opts), unlock: formatAssetUrl(iconsUnlock, opts),
'vertical-align-center': formatAssetUrl(iconsVerticalAlignCenter, opts),
'vertical-align-end': formatAssetUrl(iconsVerticalAlignEnd, opts),
'vertical-align-start': formatAssetUrl(iconsVerticalAlignStart, opts),
visible: formatAssetUrl(iconsVisible, opts), visible: formatAssetUrl(iconsVisible, opts),
'warning-triangle': formatAssetUrl(iconsWarningTriangle, opts), 'warning-triangle': formatAssetUrl(iconsWarningTriangle, opts),
'zoom-in': formatAssetUrl(iconsZoomIn, opts), 'zoom-in': formatAssetUrl(iconsZoomIn, opts),

View file

@ -165,6 +165,9 @@ export function getAssetUrlsByMetaUrl(opts?: AssetUrlOptions): {
ungroup: string ungroup: string
'unlock-small': string 'unlock-small': string
unlock: string unlock: string
'vertical-align-center': string
'vertical-align-end': string
'vertical-align-start': string
visible: string visible: string
'warning-triangle': string 'warning-triangle': string
'zoom-in': string 'zoom-in': string

View file

@ -539,6 +539,18 @@ export function getAssetUrlsByMetaUrl(opts) {
opts opts
), ),
unlock: formatAssetUrl(new URL('./icons/icon/unlock.svg', import.meta.url).href, opts), unlock: formatAssetUrl(new URL('./icons/icon/unlock.svg', import.meta.url).href, opts),
'vertical-align-center': formatAssetUrl(
new URL('./icons/icon/vertical-align-center.svg', import.meta.url).href,
opts
),
'vertical-align-end': formatAssetUrl(
new URL('./icons/icon/vertical-align-end.svg', import.meta.url).href,
opts
),
'vertical-align-start': formatAssetUrl(
new URL('./icons/icon/vertical-align-start.svg', import.meta.url).href,
opts
),
visible: formatAssetUrl(new URL('./icons/icon/visible.svg', import.meta.url).href, opts), visible: formatAssetUrl(new URL('./icons/icon/visible.svg', import.meta.url).href, opts),
'warning-triangle': formatAssetUrl( 'warning-triangle': formatAssetUrl(
new URL('./icons/icon/warning-triangle.svg', import.meta.url).href, new URL('./icons/icon/warning-triangle.svg', import.meta.url).href,

View file

@ -1720,7 +1720,7 @@ export abstract class TLBoxTool extends StateNode {
// (undocumented) // (undocumented)
abstract shapeType: string; abstract shapeType: string;
// (undocumented) // (undocumented)
styles: ("align" | "arrowheadEnd" | "arrowheadStart" | "color" | "dash" | "fill" | "font" | "geo" | "icon" | "labelColor" | "opacity" | "size" | "spline")[]; styles: ("align" | "arrowheadEnd" | "arrowheadStart" | "color" | "dash" | "fill" | "font" | "geo" | "icon" | "labelColor" | "opacity" | "size" | "spline" | "verticalAlign")[];
} }
// @public (undocumented) // @public (undocumented)
@ -2127,6 +2127,7 @@ export class TLGeoUtil extends TLBoxUtil<TLGeoShape> {
opacity: "0.1" | "0.25" | "0.5" | "0.75" | "1"; opacity: "0.1" | "0.25" | "0.5" | "0.75" | "1";
font: "draw" | "mono" | "sans" | "serif"; font: "draw" | "mono" | "sans" | "serif";
align: "end" | "middle" | "start"; align: "end" | "middle" | "start";
verticalAlign: "end" | "middle" | "start";
url: string; url: string;
w: number; w: number;
h: number; h: number;
@ -2155,6 +2156,7 @@ export class TLGeoUtil extends TLBoxUtil<TLGeoShape> {
opacity: "0.1" | "0.25" | "0.5" | "0.75" | "1"; opacity: "0.1" | "0.25" | "0.5" | "0.75" | "1";
font: "draw" | "mono" | "sans" | "serif"; font: "draw" | "mono" | "sans" | "serif";
align: "end" | "middle" | "start"; align: "end" | "middle" | "start";
verticalAlign: "end" | "middle" | "start";
url: string; url: string;
w: number; w: number;
h: number; h: number;

View file

@ -1000,7 +1000,6 @@ input,
height: 100%; height: 100%;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center;
color: var(--color-text); color: var(--color-text);
text-shadow: var(--tl-text-outline); text-shadow: var(--tl-text-outline);
overflow: hidden; overflow: hidden;

View file

@ -1053,6 +1053,7 @@ export class TLArrowUtil extends TLShapeUtil<TLArrowShape> {
fontFamily: font, fontFamily: font,
padding: 0, padding: 0,
textAlign: 'middle' as const, textAlign: 'middle' as const,
verticalTextAlign: 'middle' as const,
width: labelSize.w, width: labelSize.w,
height: labelSize.h, height: labelSize.h,
fontStyle: 'normal', fontStyle: 'normal',

View file

@ -66,6 +66,7 @@ export class TLGeoUtil extends TLBoxUtil<TLGeoShape> {
font: 'draw', font: 'draw',
text: '', text: '',
align: 'middle', align: 'middle',
verticalAlign: 'middle',
growY: 0, growY: 0,
url: '', url: '',
} }
@ -343,7 +344,8 @@ export class TLGeoUtil extends TLBoxUtil<TLGeoShape> {
const forceSolid = useForceSolid() const forceSolid = useForceSolid()
const strokeWidth = this.app.getStrokeWidth(props.size) const strokeWidth = this.app.getStrokeWidth(props.size)
const { w, color, labelColor, fill, dash, growY, font, align, size, text } = props const { w, color, labelColor, fill, dash, growY, font, align, verticalAlign, size, text } =
props
const getShape = () => { const getShape = () => {
const h = props.h + growY const h = props.h + growY
@ -448,6 +450,7 @@ export class TLGeoUtil extends TLBoxUtil<TLGeoShape> {
fill={fill} fill={fill}
size={size} size={size}
align={align} align={align}
verticalAlign={verticalAlign}
text={text} text={text}
labelColor={this.app.getCssColor(labelColor)} labelColor={this.app.getCssColor(labelColor)}
wrap wrap
@ -641,6 +644,7 @@ export class TLGeoUtil extends TLBoxUtil<TLGeoShape> {
fontSize: LABEL_FONT_SIZES[shape.props.size], fontSize: LABEL_FONT_SIZES[shape.props.size],
fontFamily: font, fontFamily: font,
textAlign: shape.props.align, textAlign: shape.props.align,
verticalTextAlign: shape.props.verticalAlign,
padding: 16, padding: 16,
lineHeight: TEXT_PROPS.lineHeight, lineHeight: TEXT_PROPS.lineHeight,
fontStyle: 'normal', fontStyle: 'normal',

View file

@ -82,6 +82,7 @@ export class TLNoteUtil extends TLShapeUtil<TLNoteShape> {
font={font} font={font}
size={size} size={size}
align={align} align={align}
verticalAlign="middle"
text={text} text={text}
labelColor="inherit" labelColor="inherit"
wrap wrap
@ -135,6 +136,7 @@ export class TLNoteUtil extends TLShapeUtil<TLNoteShape> {
fontSize: LABEL_FONT_SIZES[shape.props.size], fontSize: LABEL_FONT_SIZES[shape.props.size],
fontFamily: font, fontFamily: font,
textAlign: shape.props.align, textAlign: shape.props.align,
verticalTextAlign: 'middle' as const,
width: bounds.width - PADDING * 2, width: bounds.width - PADDING * 2,
height: bounds.height - PADDING * 2, height: bounds.height - PADDING * 2,
padding: 0, padding: 0,

View file

@ -167,6 +167,7 @@ export class TLTextUtil extends TLShapeUtil<TLTextShape> {
fontSize: FONT_SIZES[shape.props.size], fontSize: FONT_SIZES[shape.props.size],
fontFamily: font!, fontFamily: font!,
textAlign: shape.props.align, textAlign: shape.props.align,
verticalTextAlign: 'middle' as const,
width, width,
height, height,
padding: 0, // no padding? padding: 0, // no padding?

View file

@ -1,4 +1,11 @@
import { TLAlignType, TLFillType, TLFontType, TLShape, TLSizeType } from '@tldraw/tlschema' import {
TLAlignType,
TLFillType,
TLFontType,
TLShape,
TLSizeType,
TLVerticalAlignType,
} from '@tldraw/tlschema'
import React from 'react' import React from 'react'
import { LABEL_FONT_SIZES, TEXT_PROPS } from '../../../constants' import { LABEL_FONT_SIZES, TEXT_PROPS } from '../../../constants'
import { stopEventPropagation } from '../../../utils/dom' import { stopEventPropagation } from '../../../utils/dom'
@ -15,6 +22,7 @@ export const TextLabel = React.memo(function TextLabel<
labelColor, labelColor,
font, font,
align, align,
verticalAlign,
wrap, wrap,
}: { }: {
id: T['id'] id: T['id']
@ -23,6 +31,7 @@ export const TextLabel = React.memo(function TextLabel<
font: TLFontType font: TLFontType
fill?: TLFillType fill?: TLFillType
align: TLAlignType align: TLAlignType
verticalAlign: TLVerticalAlignType
wrap?: boolean wrap?: boolean
text: string text: string
labelColor: string labelColor: string
@ -48,6 +57,7 @@ export const TextLabel = React.memo(function TextLabel<
data-hastext={!isEmpty} data-hastext={!isEmpty}
data-isediting={isEditing} data-isediting={isEditing}
data-textwrap={!!wrap} data-textwrap={!!wrap}
style={{ alignItems: verticalAlign === 'middle' ? 'center' : verticalAlign }}
> >
<div <div
className="tl-text-label__inner" className="tl-text-label__inner"

View file

@ -1,4 +1,4 @@
import { TLAlignType } from '@tldraw/tlschema' import { TLAlignType, TLVerticalAlignType } from '@tldraw/tlschema'
import { TEXT_PROPS } from '../../../constants' import { TEXT_PROPS } from '../../../constants'
import { correctSpacesToNbsp } from '../../../utils/string' import { correctSpacesToNbsp } from '../../../utils/string'
import { App } from '../../App' import { App } from '../../App'
@ -11,6 +11,7 @@ export function getTextSvgElement(
fontSize: number fontSize: number
fontFamily: string fontFamily: string
textAlign: TLAlignType textAlign: TLAlignType
verticalTextAlign: TLVerticalAlignType
fontWeight: string fontWeight: string
fontStyle: string fontStyle: string
lineHeight: number lineHeight: number
@ -40,7 +41,21 @@ export function getTextSvgElement(
const innerHeight = lines.length * (opts.lineHeight * opts.fontSize) const innerHeight = lines.length * (opts.lineHeight * opts.fontSize)
const offsetY = (Math.ceil(opts.height) - innerHeight) / 2 let offsetY: number
switch (opts.verticalTextAlign) {
case 'start': {
offsetY = padding
break
}
case 'end': {
offsetY = opts.height - padding - innerHeight
break
}
default: {
offsetY = (Math.ceil(opts.height) - innerHeight) / 2
}
}
const offsetX = padding const offsetX = padding
// Create text span elements for each line // Create text span elements for each line

View file

@ -235,6 +235,11 @@ export const STYLES: TLStyleCollections = {
{ id: 'middle', type: 'align', icon: 'text-align-center' }, { id: 'middle', type: 'align', icon: 'text-align-center' },
{ id: 'end', type: 'align', icon: 'text-align-right' }, { id: 'end', type: 'align', icon: 'text-align-right' },
], ],
verticalAlign: [
{ id: 'start', type: 'verticalAlign', icon: 'vertical-align-start' },
{ id: 'middle', type: 'verticalAlign', icon: 'vertical-align-center' },
{ id: 'end', type: 'verticalAlign', icon: 'vertical-align-end' },
],
geo: [ geo: [
{ id: 'rectangle', type: 'geo', icon: 'geo-rectangle' }, { id: 'rectangle', type: 'geo', icon: 'geo-rectangle' },
{ id: 'ellipse', type: 'geo', icon: 'geo-ellipse' }, { id: 'ellipse', type: 'geo', icon: 'geo-ellipse' },

View file

@ -21,6 +21,7 @@ Array [
"size": "m", "size": "m",
"text": "", "text": "",
"url": "", "url": "",
"verticalAlign": "middle",
"w": 100, "w": 100,
}, },
"rotation": 3.141592653589793, "rotation": 3.141592653589793,
@ -48,6 +49,7 @@ Array [
"size": "m", "size": "m",
"text": "", "text": "",
"url": "", "url": "",
"verticalAlign": "middle",
"w": 100, "w": 100,
}, },
"rotation": 0, "rotation": 0,
@ -75,6 +77,7 @@ Array [
"size": "m", "size": "m",
"text": "", "text": "",
"url": "", "url": "",
"verticalAlign": "middle",
"w": 100, "w": 100,
}, },
"rotation": 0, "rotation": 0,
@ -107,6 +110,7 @@ Array [
"size": "m", "size": "m",
"text": "", "text": "",
"url": "", "url": "",
"verticalAlign": "middle",
"w": 100, "w": 100,
}, },
"rotation": 0, "rotation": 0,
@ -134,6 +138,7 @@ Array [
"size": "m", "size": "m",
"text": "", "text": "",
"url": "", "url": "",
"verticalAlign": "middle",
"w": 100, "w": 100,
}, },
"rotation": 0, "rotation": 0,
@ -161,6 +166,7 @@ Array [
"size": "m", "size": "m",
"text": "", "text": "",
"url": "", "url": "",
"verticalAlign": "middle",
"w": 100, "w": 100,
}, },
"rotation": 0, "rotation": 0,

View file

@ -31,6 +31,7 @@ describe('App.props', () => {
size: 'm', size: 'm',
font: 'draw', font: 'draw',
geo: 'rectangle', geo: 'rectangle',
verticalAlign: 'middle',
// h: 100, // h: 100,
// w: 100, // w: 100,
// growY: 0, // growY: 0,
@ -51,6 +52,7 @@ describe('App.props', () => {
size: 'm', size: 'm',
font: 'draw', font: 'draw',
geo: 'rectangle', geo: 'rectangle',
verticalAlign: 'middle',
// h: 100, // blacklisted // h: 100, // blacklisted
// w: 100, // blacklisted // w: 100, // blacklisted
// growY: 0, // blacklist // growY: 0, // blacklist
@ -81,6 +83,7 @@ describe('App.props', () => {
font: 'draw', font: 'draw',
geo: 'rectangle', geo: 'rectangle',
opacity: '1', opacity: '1',
verticalAlign: 'middle',
// h: null, // mixed! but also blacklisted // h: null, // mixed! but also blacklisted
// w: null, // mixed! but also blacklisted // w: null, // mixed! but also blacklisted
// growY: 0, // blacklist // growY: 0, // blacklist
@ -110,6 +113,7 @@ describe('App.props', () => {
w: 100, w: 100,
opacity: '0.5', opacity: '0.5',
url: 'https://aol.com', url: 'https://aol.com',
verticalAlign: 'start',
}, },
}, },
]) ])
@ -125,6 +129,7 @@ describe('App.props', () => {
size: null, size: null,
font: null, font: null,
opacity: null, opacity: null,
verticalAlign: null,
// growY: null, // blacklist // growY: null, // blacklist
// url: null, // blacklist // url: null, // blacklist
}) })

View file

@ -531,7 +531,7 @@ export const TL_SIZE_TYPES: Set<"l" | "m" | "s" | "xl">;
export const TL_SPLINE_TYPES: Set<"cubic" | "line">; export const TL_SPLINE_TYPES: Set<"cubic" | "line">;
// @public (undocumented) // @public (undocumented)
export const TL_STYLE_TYPES: Set<"align" | "arrowheadEnd" | "arrowheadStart" | "color" | "dash" | "fill" | "font" | "geo" | "icon" | "labelColor" | "opacity" | "size" | "spline">; export const TL_STYLE_TYPES: Set<"align" | "arrowheadEnd" | "arrowheadStart" | "color" | "dash" | "fill" | "font" | "geo" | "icon" | "labelColor" | "opacity" | "size" | "spline" | "verticalAlign">;
// @public (undocumented) // @public (undocumented)
export const TL_UI_COLOR_TYPES: Set<"accent" | "black" | "muted-1" | "selection-fill" | "selection-stroke" | "white">; export const TL_UI_COLOR_TYPES: Set<"accent" | "black" | "muted-1" | "selection-fill" | "selection-stroke" | "white">;
@ -544,6 +544,14 @@ export interface TLAlignStyle extends TLBaseStyle {
type: 'align'; type: 'align';
} }
// @public (undocumented)
export interface TLAlignStyle extends TLBaseStyle {
// (undocumented)
id: TLAlignType;
// (undocumented)
type: 'align';
}
// @public (undocumented) // @public (undocumented)
export type TLAlignType = SetValue<typeof TL_ALIGN_TYPES>; export type TLAlignType = SetValue<typeof TL_ALIGN_TYPES>;
@ -860,6 +868,7 @@ export type TLGeoShapeProps = {
opacity: TLOpacityType; opacity: TLOpacityType;
font: TLFontType; font: TLFontType;
align: TLAlignType; align: TLAlignType;
verticalAlign: TLVerticalAlignType;
url: string; url: string;
w: number; w: number;
h: number; h: number;
@ -1235,6 +1244,8 @@ export interface TLStyleCollections {
size: TLSizeStyle[]; size: TLSizeStyle[];
// (undocumented) // (undocumented)
spline: TLSplineTypeStyle[]; spline: TLSplineTypeStyle[];
// (undocumented)
verticalAlign: TLVerticalAlignStyle[];
} }
// @public (undocumented) // @public (undocumented)
@ -1330,6 +1341,9 @@ export const TLUserPresence: RecordType<TLUserPresence, "userId">;
// @public (undocumented) // @public (undocumented)
export type TLUserPresenceId = ID<TLUserPresence>; export type TLUserPresenceId = ID<TLUserPresence>;
// @public (undocumented)
export type TLVerticalAlignType = SetValue<typeof TL_VERTICAL_ALIGN_TYPES>;
// @public (undocumented) // @public (undocumented)
export type TLVideoAsset = TLBaseAsset<'video', { export type TLVideoAsset = TLBaseAsset<'video', {
w: number; w: number;

View file

@ -227,6 +227,7 @@ export {
type TLStyleItem, type TLStyleItem,
type TLStyleProps, type TLStyleProps,
type TLStyleType, type TLStyleType,
type TLVerticalAlignType,
} from './style-types' } from './style-types'
export { export {
TL_CURSOR_TYPES, TL_CURSOR_TYPES,

View file

@ -665,6 +665,42 @@ describe('Adding check-box to geo shape', () => {
}) })
}) })
describe('Add verticalAlign to geo shape', () => {
const { up, down } = geoShapeMigrations.migrators[5]
test('up works as expected', () => {
expect(up({ props: { type: 'ellipse' } })).toEqual({
props: { type: 'ellipse', verticalAlign: 'middle' },
})
})
test('down works as expected', () => {
expect(down({ props: { verticalAlign: 'middle', type: 'ellipse' } })).toEqual({
props: { type: 'ellipse' },
})
})
})
describe('Add verticalAlign to props for next shape', () => {
const { up, down } = instanceTypeMigrations.migrators[9]
test('up works as expected', () => {
expect(up({ propsForNextShape: { color: 'red' } })).toEqual({
propsForNextShape: {
color: 'red',
verticalAlign: 'middle',
},
})
})
test('down works as expected', () => {
const instance = { propsForNextShape: { color: 'red', verticalAlign: 'middle' } }
expect(down(instance)).toEqual({
propsForNextShape: {
color: 'red',
},
})
})
})
describe('Removing isReadOnly from user_document', () => { describe('Removing isReadOnly from user_document', () => {
const { up, down } = userDocumentTypeMigrations.migrators[userDocumentVersions.RemoveIsReadOnly] const { up, down } = userDocumentTypeMigrations.migrators[userDocumentVersions.RemoveIsReadOnly]
const prev = { const prev = {

View file

@ -18,6 +18,7 @@ import {
sizeValidator, sizeValidator,
splineValidator, splineValidator,
userIdValidator, userIdValidator,
verticalAlignValidator,
} from '../validation' } from '../validation'
import { TLPageId } from './TLPage' import { TLPageId } from './TLPage'
import { TLShapeProps } from './TLShape' import { TLShapeProps } from './TLShape'
@ -72,6 +73,7 @@ export const instanceTypeValidator: T.Validator<TLInstance> = T.model(
opacity: opacityValidator, opacity: opacityValidator,
font: fontValidator, font: fontValidator,
align: alignValidator, align: alignValidator,
verticalAlign: verticalAlignValidator,
icon: iconValidator, icon: iconValidator,
geo: geoValidator, geo: geoValidator,
arrowheadStart: arrowheadValidator, arrowheadStart: arrowheadValidator,
@ -102,13 +104,14 @@ const Versions = {
AddFollowingUserId: 6, AddFollowingUserId: 6,
RemoveAlignJustify: 7, RemoveAlignJustify: 7,
AddZoom: 8, AddZoom: 8,
AddVerticalAlign: 9,
} as const } as const
/** @public */ /** @public */
export const instanceTypeMigrations = defineMigrations({ export const instanceTypeMigrations = defineMigrations({
firstVersion: Versions.Initial, firstVersion: Versions.Initial,
// STEP 2: Update the current version to point to your latest version // STEP 2: Update the current version to point to your latest version
currentVersion: Versions.AddZoom, currentVersion: Versions.AddVerticalAlign,
// STEP 3: Add an up+down migration for the new version here // STEP 3: Add an up+down migration for the new version here
migrators: { migrators: {
[Versions.AddTransparentExportBgs]: { [Versions.AddTransparentExportBgs]: {
@ -206,6 +209,24 @@ export const instanceTypeMigrations = defineMigrations({
return instance return instance
}, },
}, },
[Versions.AddVerticalAlign]: {
up: (instance: TLInstance) => {
return {
...instance,
propsForNextShape: {
...instance.propsForNextShape,
verticalAlign: 'middle',
},
}
},
down: (instance: TLInstance) => {
const { verticalAlign: _, ...propsForNextShape } = instance.propsForNextShape
return {
...instance,
propsForNextShape,
}
},
},
}, },
}) })
@ -227,6 +248,7 @@ export const TLInstance = createRecordType<TLInstance>('instance', {
icon: 'file', icon: 'file',
font: 'draw', font: 'draw',
align: 'middle', align: 'middle',
verticalAlign: 'middle',
geo: 'rectangle', geo: 'rectangle',
arrowheadStart: 'none', arrowheadStart: 'none',
arrowheadEnd: 'arrow', arrowheadEnd: 'arrow',

View file

@ -9,6 +9,7 @@ import {
TLGeoType, TLGeoType,
TLOpacityType, TLOpacityType,
TLSizeType, TLSizeType,
TLVerticalAlignType,
} from '../style-types' } from '../style-types'
import { import {
alignValidator, alignValidator,
@ -19,6 +20,7 @@ import {
geoValidator, geoValidator,
opacityValidator, opacityValidator,
sizeValidator, sizeValidator,
verticalAlignValidator,
} from '../validation' } from '../validation'
import { TLBaseShape, createShapeValidator } from './shape-validation' import { TLBaseShape, createShapeValidator } from './shape-validation'
@ -33,6 +35,7 @@ export type TLGeoShapeProps = {
opacity: TLOpacityType opacity: TLOpacityType
font: TLFontType font: TLFontType
align: TLAlignType align: TLAlignType
verticalAlign: TLVerticalAlignType
url: string url: string
w: number w: number
h: number h: number
@ -57,6 +60,7 @@ export const geoShapeTypeValidator: T.Validator<TLGeoShape> = createShapeValidat
opacity: opacityValidator, opacity: opacityValidator,
font: fontValidator, font: fontValidator,
align: alignValidator, align: alignValidator,
verticalAlign: verticalAlignValidator,
url: T.string, url: T.string,
w: T.nonZeroNumber, w: T.nonZeroNumber,
h: T.nonZeroNumber, h: T.nonZeroNumber,
@ -74,13 +78,14 @@ const Versions = {
AddLabelColor: 2, AddLabelColor: 2,
RemoveJustify: 3, RemoveJustify: 3,
AddCheckBox: 4, AddCheckBox: 4,
AddVerticalAlign: 5,
} as const } as const
/** @public */ /** @public */
export const geoShapeMigrations = defineMigrations({ export const geoShapeMigrations = defineMigrations({
// STEP 2: Update the current version to point to your latest version // STEP 2: Update the current version to point to your latest version
firstVersion: Versions.Initial, firstVersion: Versions.Initial,
currentVersion: Versions.AddCheckBox, currentVersion: Versions.AddVerticalAlign,
migrators: { migrators: {
// STEP 3: Add an up+down migration for the new version here // STEP 3: Add an up+down migration for the new version here
[Versions.AddUrlProp]: { [Versions.AddUrlProp]: {
@ -143,5 +148,23 @@ export const geoShapeMigrations = defineMigrations({
} }
}, },
}, },
[Versions.AddVerticalAlign]: {
up: (shape) => {
return {
...shape,
props: {
...shape.props,
verticalAlign: 'middle',
},
}
},
down: (shape) => {
const { verticalAlign: _, ...props } = shape.props
return {
...shape,
props,
}
},
},
}, },
}) })

View file

@ -11,6 +11,7 @@ export const TL_STYLE_TYPES = new Set([
'opacity', 'opacity',
'font', 'font',
'align', 'align',
'verticalAlign',
'icon', 'icon',
'geo', 'geo',
'arrowheadStart', 'arrowheadStart',
@ -133,6 +134,25 @@ export interface TLAlignStyle extends TLBaseStyle {
type: 'align' type: 'align'
} }
/** @public */
export interface TLAlignStyle extends TLBaseStyle {
id: TLAlignType
type: 'align'
}
// Geo Text Vertical Align
/** @public */
export const TL_VERTICAL_ALIGN_TYPES = TL_ALIGN_TYPES
/** @public */
export type TLVerticalAlignType = SetValue<typeof TL_VERTICAL_ALIGN_TYPES>
/** @public */
export interface TLVerticalAlignStyle extends TLBaseStyle {
id: TLVerticalAlignType
type: 'verticalAlign'
}
// Geo // Geo
/** @public */ /** @public */
@ -527,6 +547,7 @@ export interface TLStyleCollections {
opacity: TLOpacityStyle[] opacity: TLOpacityStyle[]
font: TLFontStyle[] font: TLFontStyle[]
align: TLAlignStyle[] align: TLAlignStyle[]
verticalAlign: TLVerticalAlignStyle[]
geo: TLGeoStyle[] geo: TLGeoStyle[]
arrowheadStart: TLArrowheadStartStyle[] arrowheadStart: TLArrowheadStartStyle[]
arrowheadEnd: TLArrowheadEndStyle[] arrowheadEnd: TLArrowheadEndStyle[]

View file

@ -18,6 +18,7 @@ import {
TL_OPACITY_TYPES, TL_OPACITY_TYPES,
TL_SIZE_TYPES, TL_SIZE_TYPES,
TL_SPLINE_TYPES, TL_SPLINE_TYPES,
TL_VERTICAL_ALIGN_TYPES,
} from './style-types' } from './style-types'
/** @internal */ /** @internal */
@ -65,6 +66,8 @@ export const fontValidator = T.setEnum(TL_FONT_TYPES)
/** @internal */ /** @internal */
export const alignValidator = T.setEnum(TL_ALIGN_TYPES) export const alignValidator = T.setEnum(TL_ALIGN_TYPES)
/** @internal */ /** @internal */
export const verticalAlignValidator = T.setEnum(TL_VERTICAL_ALIGN_TYPES)
/** @internal */
export const arrowheadValidator = T.setEnum(TL_ARROWHEAD_TYPES) export const arrowheadValidator = T.setEnum(TL_ARROWHEAD_TYPES)
/** @internal */ /** @internal */
export const opacityValidator = T.setEnum(TL_OPACITY_TYPES) export const opacityValidator = T.setEnum(TL_OPACITY_TYPES)

File diff suppressed because one or more lines are too long

View file

@ -12,7 +12,7 @@ type AllStyles = typeof App.styles
interface DropdownPickerProps<T extends AllStyles[keyof AllStyles][number]> { interface DropdownPickerProps<T extends AllStyles[keyof AllStyles][number]> {
id: string id: string
label: TLTranslationKey label?: TLTranslationKey
items: T[] items: T[]
styleType: TLStyleType styleType: TLStyleType
value: T['id'] | null value: T['id'] | null
@ -52,7 +52,8 @@ export const DropdownPicker = React.memo(function DropdownPicker<
<DropdownMenu.Content side="left" align="center" alignOffset={0}> <DropdownMenu.Content side="left" align="center" alignOffset={0}>
<div <div
className={classNames('tlui-button-grid', { className={classNames('tlui-button-grid', {
'tlui-button-grid__two': items.length < 4, 'tlui-button-grid__two': items.length < 3,
'tlui-button-grid__three': items.length == 3,
'tlui-button-grid__four': items.length >= 4, 'tlui-button-grid__four': items.length >= 4,
})} })}
> >

View file

@ -1,5 +1,6 @@
import { App, TLNullableShapeProps, TLStyleItem, useApp } from '@tldraw/editor' import { App, TLNullableShapeProps, TLStyleItem, useApp } from '@tldraw/editor'
import React, { useCallback } from 'react' import React, { useCallback } from 'react'
import { useValue } from 'signia-react' import { useValue } from 'signia-react'
import { useTranslation } from '../../hooks/useTranslation/useTranslation' import { useTranslation } from '../../hooks/useTranslation/useTranslation'
import { Button } from '../primitives/Button' import { Button } from '../primitives/Button'
@ -161,7 +162,7 @@ function TextStylePickerSet({ props }: { props: TLNullableShapeProps }) {
const msg = useTranslation() const msg = useTranslation()
const handleValueChange = useStyleChangeCallback() const handleValueChange = useStyleChangeCallback()
const { font, align } = props const { font, align, verticalAlign } = props
if (font === undefined && align === undefined) { if (font === undefined && align === undefined) {
return null return null
} }
@ -189,12 +190,23 @@ function TextStylePickerSet({ props }: { props: TLNullableShapeProps }) {
value={align} value={align}
onValueChange={handleValueChange} onValueChange={handleValueChange}
/> />
<Button {verticalAlign === undefined ? (
title={msg('style-panel.position')} <Button
data-wd="position" title={msg('style-panel.vertical-align')}
icon="align-center-center" data-wd="vertical-align"
disabled icon="vertical-align-center"
/> disabled
/>
) : (
<DropdownPicker
id="geo-vertical-alignment"
styleType="verticalAlign"
data-wd="style-panel.geo-vertical-align"
items={styles.verticalAlign}
value={verticalAlign}
onValueChange={handleValueChange}
/>
)}
</div> </div>
)} )}
</div> </div>

View file

@ -276,6 +276,7 @@ export type TLTranslationKey =
| 'shortcuts-dialog.view' | 'shortcuts-dialog.view'
| 'style-panel.title' | 'style-panel.title'
| 'style-panel.align' | 'style-panel.align'
| 'style-panel.vertical-align'
| 'style-panel.position' | 'style-panel.position'
| 'style-panel.arrowheads' | 'style-panel.arrowheads'
| 'style-panel.arrowhead-start' | 'style-panel.arrowhead-start'

View file

@ -279,6 +279,7 @@ export const DEFAULT_TRANSLATION = {
'shortcuts-dialog.view': 'View', 'shortcuts-dialog.view': 'View',
'style-panel.title': 'Styles', 'style-panel.title': 'Styles',
'style-panel.align': 'Align', 'style-panel.align': 'Align',
'style-panel.vertical-align': 'Vertical align',
'style-panel.position': 'Position', 'style-panel.position': 'Position',
'style-panel.arrowheads': 'Arrowheads', 'style-panel.arrowheads': 'Arrowheads',
'style-panel.arrowhead-start': 'Start', 'style-panel.arrowhead-start': 'Start',

View file

@ -156,6 +156,9 @@ export type TLUiIconType =
| 'ungroup' | 'ungroup'
| 'unlock-small' | 'unlock-small'
| 'unlock' | 'unlock'
| 'vertical-align-center'
| 'vertical-align-end'
| 'vertical-align-start'
| 'visible' | 'visible'
| 'warning-triangle' | 'warning-triangle'
| 'zoom-in' | 'zoom-in'
@ -316,6 +319,9 @@ export const TLUiIconTypes = [
'ungroup', 'ungroup',
'unlock-small', 'unlock-small',
'unlock', 'unlock',
'vertical-align-center',
'vertical-align-end',
'vertical-align-start',
'visible', 'visible',
'warning-triangle', 'warning-triangle',
'zoom-in', 'zoom-in',