@tldraw/editor 3.15.0-next.f1dfcef63951 → 3.16.0-next.c30b1b5e551a

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. package/dist-cjs/index.d.ts +159 -44
  2. package/dist-cjs/index.js +20 -16
  3. package/dist-cjs/index.js.map +2 -2
  4. package/dist-cjs/lib/components/SVGContainer.js +1 -1
  5. package/dist-cjs/lib/components/SVGContainer.js.map +2 -2
  6. package/dist-cjs/lib/components/Shape.js +4 -26
  7. package/dist-cjs/lib/components/Shape.js.map +2 -2
  8. package/dist-cjs/lib/components/default-components/DefaultBrush.js +1 -1
  9. package/dist-cjs/lib/components/default-components/DefaultBrush.js.map +2 -2
  10. package/dist-cjs/lib/components/default-components/DefaultCanvas.js +1 -1
  11. package/dist-cjs/lib/components/default-components/DefaultCanvas.js.map +2 -2
  12. package/dist-cjs/lib/components/default-components/DefaultCollaboratorHint.js +1 -1
  13. package/dist-cjs/lib/components/default-components/DefaultCollaboratorHint.js.map +2 -2
  14. package/dist-cjs/lib/components/default-components/DefaultCursor.js +1 -1
  15. package/dist-cjs/lib/components/default-components/DefaultCursor.js.map +2 -2
  16. package/dist-cjs/lib/components/default-components/DefaultGrid.js +1 -1
  17. package/dist-cjs/lib/components/default-components/DefaultGrid.js.map +2 -2
  18. package/dist-cjs/lib/components/default-components/DefaultHandles.js +1 -1
  19. package/dist-cjs/lib/components/default-components/DefaultHandles.js.map +2 -2
  20. package/dist-cjs/lib/components/default-components/DefaultShapeIndicator.js +1 -1
  21. package/dist-cjs/lib/components/default-components/DefaultShapeIndicator.js.map +2 -2
  22. package/dist-cjs/lib/components/default-components/DefaultShapeWrapper.js +53 -0
  23. package/dist-cjs/lib/components/default-components/DefaultShapeWrapper.js.map +7 -0
  24. package/dist-cjs/lib/components/default-components/DefaultSnapIndictor.js +1 -1
  25. package/dist-cjs/lib/components/default-components/DefaultSnapIndictor.js.map +2 -2
  26. package/dist-cjs/lib/components/default-components/DefaultSpinner.js +27 -15
  27. package/dist-cjs/lib/components/default-components/DefaultSpinner.js.map +3 -3
  28. package/dist-cjs/lib/config/TLUserPreferences.js +7 -1
  29. package/dist-cjs/lib/config/TLUserPreferences.js.map +2 -2
  30. package/dist-cjs/lib/editor/Editor.js +88 -43
  31. package/dist-cjs/lib/editor/Editor.js.map +2 -2
  32. package/dist-cjs/lib/editor/managers/TextManager/TextManager.js +96 -101
  33. package/dist-cjs/lib/editor/managers/TextManager/TextManager.js.map +2 -2
  34. package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js +7 -2
  35. package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js.map +2 -2
  36. package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +2 -2
  37. package/dist-cjs/lib/editor/tools/StateNode.js +20 -1
  38. package/dist-cjs/lib/editor/tools/StateNode.js.map +2 -2
  39. package/dist-cjs/lib/editor/types/misc-types.js.map +1 -1
  40. package/dist-cjs/lib/hooks/useEditorComponents.js +2 -0
  41. package/dist-cjs/lib/hooks/useEditorComponents.js.map +2 -2
  42. package/dist-cjs/lib/license/Watermark.js +2 -2
  43. package/dist-cjs/lib/license/Watermark.js.map +2 -2
  44. package/dist-cjs/lib/primitives/geometry/Arc2d.js +1 -1
  45. package/dist-cjs/lib/primitives/geometry/Arc2d.js.map +2 -2
  46. package/dist-cjs/lib/primitives/geometry/Circle2d.js +1 -1
  47. package/dist-cjs/lib/primitives/geometry/Circle2d.js.map +2 -2
  48. package/dist-cjs/lib/primitives/geometry/CubicBezier2d.js +3 -1
  49. package/dist-cjs/lib/primitives/geometry/CubicBezier2d.js.map +2 -2
  50. package/dist-cjs/lib/primitives/geometry/Ellipse2d.js +1 -1
  51. package/dist-cjs/lib/primitives/geometry/Ellipse2d.js.map +2 -2
  52. package/dist-cjs/lib/primitives/geometry/geometry-constants.js +2 -2
  53. package/dist-cjs/lib/primitives/geometry/geometry-constants.js.map +2 -2
  54. package/dist-cjs/lib/primitives/intersect.js +4 -4
  55. package/dist-cjs/lib/primitives/intersect.js.map +2 -2
  56. package/dist-cjs/lib/primitives/utils.js +4 -0
  57. package/dist-cjs/lib/primitives/utils.js.map +2 -2
  58. package/dist-cjs/lib/utils/sync/TLLocalSyncClient.js +0 -1
  59. package/dist-cjs/lib/utils/sync/TLLocalSyncClient.js.map +2 -2
  60. package/dist-cjs/version.js +3 -3
  61. package/dist-cjs/version.js.map +1 -1
  62. package/dist-esm/index.d.mts +159 -44
  63. package/dist-esm/index.mjs +47 -41
  64. package/dist-esm/index.mjs.map +2 -2
  65. package/dist-esm/lib/components/SVGContainer.mjs +1 -1
  66. package/dist-esm/lib/components/SVGContainer.mjs.map +2 -2
  67. package/dist-esm/lib/components/Shape.mjs +4 -26
  68. package/dist-esm/lib/components/Shape.mjs.map +2 -2
  69. package/dist-esm/lib/components/default-components/DefaultBrush.mjs +1 -1
  70. package/dist-esm/lib/components/default-components/DefaultBrush.mjs.map +2 -2
  71. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs +1 -1
  72. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs.map +2 -2
  73. package/dist-esm/lib/components/default-components/DefaultCollaboratorHint.mjs +1 -1
  74. package/dist-esm/lib/components/default-components/DefaultCollaboratorHint.mjs.map +2 -2
  75. package/dist-esm/lib/components/default-components/DefaultCursor.mjs +1 -1
  76. package/dist-esm/lib/components/default-components/DefaultCursor.mjs.map +2 -2
  77. package/dist-esm/lib/components/default-components/DefaultGrid.mjs +1 -1
  78. package/dist-esm/lib/components/default-components/DefaultGrid.mjs.map +2 -2
  79. package/dist-esm/lib/components/default-components/DefaultHandles.mjs +1 -1
  80. package/dist-esm/lib/components/default-components/DefaultHandles.mjs.map +2 -2
  81. package/dist-esm/lib/components/default-components/DefaultShapeIndicator.mjs +1 -1
  82. package/dist-esm/lib/components/default-components/DefaultShapeIndicator.mjs.map +2 -2
  83. package/dist-esm/lib/components/default-components/DefaultShapeWrapper.mjs +23 -0
  84. package/dist-esm/lib/components/default-components/DefaultShapeWrapper.mjs.map +7 -0
  85. package/dist-esm/lib/components/default-components/DefaultSnapIndictor.mjs +1 -1
  86. package/dist-esm/lib/components/default-components/DefaultSnapIndictor.mjs.map +2 -2
  87. package/dist-esm/lib/components/default-components/DefaultSpinner.mjs +17 -15
  88. package/dist-esm/lib/components/default-components/DefaultSpinner.mjs.map +2 -2
  89. package/dist-esm/lib/config/TLUserPreferences.mjs +7 -1
  90. package/dist-esm/lib/config/TLUserPreferences.mjs.map +2 -2
  91. package/dist-esm/lib/editor/Editor.mjs +88 -43
  92. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  93. package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs +96 -101
  94. package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs.map +2 -2
  95. package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs +7 -2
  96. package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs.map +2 -2
  97. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +2 -2
  98. package/dist-esm/lib/editor/tools/StateNode.mjs +20 -1
  99. package/dist-esm/lib/editor/tools/StateNode.mjs.map +2 -2
  100. package/dist-esm/lib/hooks/useEditorComponents.mjs +4 -0
  101. package/dist-esm/lib/hooks/useEditorComponents.mjs.map +2 -2
  102. package/dist-esm/lib/license/Watermark.mjs +2 -2
  103. package/dist-esm/lib/license/Watermark.mjs.map +2 -2
  104. package/dist-esm/lib/primitives/geometry/Arc2d.mjs +2 -2
  105. package/dist-esm/lib/primitives/geometry/Arc2d.mjs.map +2 -2
  106. package/dist-esm/lib/primitives/geometry/Circle2d.mjs +2 -2
  107. package/dist-esm/lib/primitives/geometry/Circle2d.mjs.map +2 -2
  108. package/dist-esm/lib/primitives/geometry/CubicBezier2d.mjs +3 -1
  109. package/dist-esm/lib/primitives/geometry/CubicBezier2d.mjs.map +2 -2
  110. package/dist-esm/lib/primitives/geometry/Ellipse2d.mjs +2 -2
  111. package/dist-esm/lib/primitives/geometry/Ellipse2d.mjs.map +2 -2
  112. package/dist-esm/lib/primitives/geometry/geometry-constants.mjs +2 -2
  113. package/dist-esm/lib/primitives/geometry/geometry-constants.mjs.map +2 -2
  114. package/dist-esm/lib/primitives/intersect.mjs +5 -5
  115. package/dist-esm/lib/primitives/intersect.mjs.map +2 -2
  116. package/dist-esm/lib/primitives/utils.mjs +4 -0
  117. package/dist-esm/lib/primitives/utils.mjs.map +2 -2
  118. package/dist-esm/lib/utils/sync/TLLocalSyncClient.mjs +0 -1
  119. package/dist-esm/lib/utils/sync/TLLocalSyncClient.mjs.map +2 -2
  120. package/dist-esm/version.mjs +3 -3
  121. package/dist-esm/version.mjs.map +1 -1
  122. package/editor.css +21 -27
  123. package/package.json +9 -8
  124. package/src/index.ts +68 -62
  125. package/src/lib/components/SVGContainer.tsx +1 -1
  126. package/src/lib/components/Shape.tsx +6 -21
  127. package/src/lib/components/default-components/DefaultBrush.tsx +1 -1
  128. package/src/lib/components/default-components/DefaultCanvas.tsx +1 -1
  129. package/src/lib/components/default-components/DefaultCollaboratorHint.tsx +1 -1
  130. package/src/lib/components/default-components/DefaultCursor.tsx +1 -1
  131. package/src/lib/components/default-components/DefaultGrid.tsx +1 -1
  132. package/src/lib/components/default-components/DefaultHandles.tsx +5 -1
  133. package/src/lib/components/default-components/DefaultShapeIndicator.tsx +1 -1
  134. package/src/lib/components/default-components/DefaultShapeWrapper.tsx +35 -0
  135. package/src/lib/components/default-components/DefaultSnapIndictor.tsx +1 -1
  136. package/src/lib/components/default-components/DefaultSpinner.tsx +12 -12
  137. package/src/lib/config/TLUserPreferences.ts +7 -0
  138. package/src/lib/editor/Editor.test.ts +407 -0
  139. package/src/lib/editor/Editor.ts +106 -44
  140. package/src/lib/editor/managers/TextManager/TextManager.ts +108 -128
  141. package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.test.ts +21 -0
  142. package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.ts +8 -0
  143. package/src/lib/editor/shapes/ShapeUtil.ts +57 -0
  144. package/src/lib/editor/tools/StateNode.test.ts +285 -0
  145. package/src/lib/editor/tools/StateNode.ts +27 -1
  146. package/src/lib/editor/types/misc-types.ts +19 -0
  147. package/src/lib/hooks/useEditorComponents.tsx +8 -2
  148. package/src/lib/license/LicenseManager.test.ts +1 -1
  149. package/src/lib/license/Watermark.tsx +2 -2
  150. package/src/lib/primitives/geometry/Arc2d.ts +2 -2
  151. package/src/lib/primitives/geometry/Circle2d.ts +2 -2
  152. package/src/lib/primitives/geometry/CubicBezier2d.ts +4 -1
  153. package/src/lib/primitives/geometry/Ellipse2d.ts +2 -2
  154. package/src/lib/primitives/geometry/geometry-constants.ts +2 -1
  155. package/src/lib/primitives/intersect.test.ts +946 -0
  156. package/src/lib/primitives/intersect.ts +12 -5
  157. package/src/lib/primitives/utils.ts +11 -0
  158. package/src/lib/utils/sync/TLLocalSyncClient.ts +0 -1
  159. package/src/version.ts +3 -3
  160. package/src/lib/test/currentToolIdMask.test.ts +0 -49
@@ -7,7 +7,7 @@ export type SVGContainerProps = React.ComponentProps<'svg'>
7
7
  /** @public @react */
8
8
  export function SVGContainer({ children, className = '', ...rest }: SVGContainerProps) {
9
9
  return (
10
- <svg {...rest} className={classNames('tl-svg-container', className)}>
10
+ <svg {...rest} className={classNames('tl-svg-container', className)} aria-hidden="true">
11
11
  {children}
12
12
  </svg>
13
13
  )
@@ -40,7 +40,7 @@ export const Shape = memo(function Shape({
40
40
  }) {
41
41
  const editor = useEditor()
42
42
 
43
- const { ShapeErrorFallback } = useEditorComponents()
43
+ const { ShapeErrorFallback, ShapeWrapper } = useEditorComponents()
44
44
 
45
45
  const containerRef = useRef<HTMLDivElement>(null)
46
46
  const bgContainerRef = useRef<HTMLDivElement>(null)
@@ -145,37 +145,22 @@ export const Shape = memo(function Shape({
145
145
  [editor]
146
146
  )
147
147
 
148
- if (!shape) return null
149
-
150
- const isFilledShape = 'fill' in shape.props && shape.props.fill !== 'none'
148
+ if (!shape || !ShapeWrapper) return null
151
149
 
152
150
  return (
153
151
  <>
154
152
  {util.backgroundComponent && (
155
- <div
156
- ref={bgContainerRef}
157
- className="tl-shape tl-shape-background"
158
- data-shape-type={shape.type}
159
- data-shape-id={shape.id}
160
- draggable={false}
161
- >
153
+ <ShapeWrapper ref={bgContainerRef} shape={shape} isBackground={true}>
162
154
  <OptionalErrorBoundary fallback={ShapeErrorFallback} onError={annotateError}>
163
155
  <InnerShapeBackground shape={shape} util={util} />
164
156
  </OptionalErrorBoundary>
165
- </div>
157
+ </ShapeWrapper>
166
158
  )}
167
- <div
168
- ref={containerRef}
169
- className="tl-shape"
170
- data-shape-type={shape.type}
171
- data-shape-is-filled={isFilledShape}
172
- data-shape-id={shape.id}
173
- draggable={false}
174
- >
159
+ <ShapeWrapper ref={containerRef} shape={shape} isBackground={false}>
175
160
  <OptionalErrorBoundary fallback={ShapeErrorFallback as any} onError={annotateError}>
176
161
  <InnerShape shape={shape} util={util} />
177
162
  </OptionalErrorBoundary>
178
- </div>
163
+ </ShapeWrapper>
179
164
  </>
180
165
  )
181
166
  })
@@ -21,7 +21,7 @@ export const DefaultBrush = ({ brush, color, opacity, className }: TLBrushProps)
21
21
  const h = toDomPrecision(Math.max(1, brush.h))
22
22
 
23
23
  return (
24
- <svg className="tl-overlays__item" ref={rSvg}>
24
+ <svg className="tl-overlays__item" ref={rSvg} aria-hidden="true">
25
25
  {color ? (
26
26
  <g className="tl-brush" opacity={opacity}>
27
27
  <rect width={w} height={h} fill={color} opacity={0.75} />
@@ -139,7 +139,7 @@ export function DefaultCanvas({ className }: TLCanvasComponentProps) {
139
139
  data-testid="canvas"
140
140
  {...events}
141
141
  >
142
- <svg className="tl-svg-context">
142
+ <svg className="tl-svg-context" aria-hidden="true">
143
143
  <defs>
144
144
  {shapeSvgDefs}
145
145
  <CursorDef />
@@ -39,7 +39,7 @@ export function DefaultCollaboratorHint({
39
39
  const cursorHintId = useSharedSafeId('cursor_hint')
40
40
 
41
41
  return (
42
- <svg ref={rSvg} className={classNames('tl-overlays__item', className)}>
42
+ <svg ref={rSvg} className={classNames('tl-overlays__item', className)} aria-hidden="true">
43
43
  <use
44
44
  href={`#${cursorHintId}`}
45
45
  color={color}
@@ -33,7 +33,7 @@ export const DefaultCursor = memo(function DefaultCursor({
33
33
 
34
34
  return (
35
35
  <div ref={rCursor} className={classNames('tl-overlays__item', className)}>
36
- <svg className="tl-cursor">
36
+ <svg className="tl-cursor" aria-hidden="true">
37
37
  <use href={`#${cursorId}`} color={color} />
38
38
  </svg>
39
39
  {chatMessage ? (
@@ -16,7 +16,7 @@ export function DefaultGrid({ x, y, z, size }: TLGridProps) {
16
16
  const editor = useEditor()
17
17
  const { gridSteps } = editor.options
18
18
  return (
19
- <svg className="tl-grid" version="1.1" xmlns="http://www.w3.org/2000/svg">
19
+ <svg className="tl-grid" version="1.1" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
20
20
  <defs>
21
21
  {gridSteps.map(({ min, mid, step }, i) => {
22
22
  const s = step * size * z
@@ -7,5 +7,9 @@ export interface TLHandlesProps {
7
7
 
8
8
  /** @public @react */
9
9
  export const DefaultHandles = ({ children }: TLHandlesProps) => {
10
- return <svg className="tl-user-handles tl-overlays__item">{children}</svg>
10
+ return (
11
+ <svg className="tl-user-handles tl-overlays__item" aria-hidden="true">
12
+ {children}
13
+ </svg>
14
+ )
11
15
  }
@@ -86,7 +86,7 @@ export const DefaultShapeIndicator = memo(function DefaultShapeIndicator({
86
86
  }, [hidden])
87
87
 
88
88
  return (
89
- <svg ref={rIndicator} className={classNames('tl-overlays__item', className)}>
89
+ <svg ref={rIndicator} className={classNames('tl-overlays__item', className)} aria-hidden="true">
90
90
  <g className="tl-shape-indicator" stroke={color ?? 'var(--color-selected)'} opacity={opacity}>
91
91
  <InnerIndicator editor={editor} id={shapeId} />
92
92
  </g>
@@ -0,0 +1,35 @@
1
+ import { TLShape } from '@tldraw/tlschema'
2
+ import classNames from 'classnames'
3
+ import { forwardRef, ReactNode } from 'react'
4
+
5
+ /** @public */
6
+ export interface TLShapeWrapperProps extends React.HTMLAttributes<HTMLDivElement> {
7
+ /** The shape being rendered. */
8
+ shape: TLShape
9
+ /** Whether this is the shapes regular, or background component. */
10
+ isBackground: boolean
11
+ /** The shape's rendered component. */
12
+ children: ReactNode
13
+ }
14
+
15
+ /** @public @react */
16
+ export const DefaultShapeWrapper = forwardRef(function DefaultShapeWrapper(
17
+ { children, shape, isBackground, ...props }: TLShapeWrapperProps,
18
+ ref: React.Ref<HTMLDivElement>
19
+ ) {
20
+ const isFilledShape = 'fill' in shape.props && shape.props.fill !== 'none'
21
+
22
+ return (
23
+ <div
24
+ ref={ref}
25
+ data-shape-type={shape.type}
26
+ data-shape-is-filled={isBackground ? undefined : isFilledShape}
27
+ data-shape-id={shape.id}
28
+ draggable={false}
29
+ {...props}
30
+ className={classNames('tl-shape', isBackground && 'tl-shape-background', props.className)}
31
+ >
32
+ {children}
33
+ </div>
34
+ )
35
+ })
@@ -163,7 +163,7 @@ export interface TLSnapIndicatorProps {
163
163
  /** @public @react */
164
164
  export function DefaultSnapIndicator({ className, line, zoom }: TLSnapIndicatorProps) {
165
165
  return (
166
- <svg className={classNames('tl-overlays__item', className)}>
166
+ <svg className={classNames('tl-overlays__item', className)} aria-hidden="true">
167
167
  {line.type === 'points' ? (
168
168
  <PointsSnapIndicator {...line} zoom={zoom} />
169
169
  ) : line.type === 'gaps' ? (
@@ -1,19 +1,19 @@
1
+ import classNames from 'classnames'
2
+
1
3
  /** @public @react */
2
- export function DefaultSpinner() {
4
+ export function DefaultSpinner(props: React.SVGProps<SVGSVGElement>) {
3
5
  return (
4
- <svg width={16} height={16} viewBox="0 0 16 16" aria-hidden="false">
6
+ <svg
7
+ width={16}
8
+ height={16}
9
+ viewBox="0 0 16 16"
10
+ aria-hidden="false"
11
+ {...props}
12
+ className={classNames('tl-spinner', props.className)}
13
+ >
5
14
  <g strokeWidth={2} fill="none" fillRule="evenodd">
6
15
  <circle strokeOpacity={0.25} cx={8} cy={8} r={7} stroke="currentColor" />
7
- <path strokeLinecap="round" d="M15 8c0-4.5-4.5-7-7-7" stroke="currentColor">
8
- <animateTransform
9
- attributeName="transform"
10
- type="rotate"
11
- from="0 8 8"
12
- to="360 8 8"
13
- dur="1s"
14
- repeatCount="indefinite"
15
- />
16
- </path>
16
+ <path strokeLinecap="round" d="M15 8c0-4.5-4.5-7-7-7" stroke="currentColor" />
17
17
  </g>
18
18
  </svg>
19
19
  )
@@ -17,6 +17,7 @@ export interface TLUserPreferences {
17
17
  // N.B. These are duplicated in TLdrawAppUser.
18
18
  locale?: string | null
19
19
  animationSpeed?: number | null
20
+ areKeyboardShortcutsEnabled?: boolean | null
20
21
  edgeScrollSpeed?: number | null
21
22
  colorScheme?: 'light' | 'dark' | 'system'
22
23
  isSnapMode?: boolean | null
@@ -44,6 +45,7 @@ export const userTypeValidator: T.Validator<TLUserPreferences> = T.object<TLUser
44
45
  // N.B. These are duplicated in TLdrawAppUser.
45
46
  locale: T.string.nullable().optional(),
46
47
  animationSpeed: T.number.nullable().optional(),
48
+ areKeyboardShortcutsEnabled: T.boolean.nullable().optional(),
47
49
  edgeScrollSpeed: T.number.nullable().optional(),
48
50
  colorScheme: T.literalEnum('light', 'dark', 'system').optional(),
49
51
  isSnapMode: T.boolean.nullable().optional(),
@@ -61,6 +63,7 @@ const Versions = {
61
63
  AddDynamicSizeMode: 6,
62
64
  AllowSystemColorScheme: 7,
63
65
  AddPasteAtCursor: 8,
66
+ AddKeyboardShortcuts: 9,
64
67
  } as const
65
68
 
66
69
  const CURRENT_VERSION = Math.max(...Object.values(Versions))
@@ -96,6 +99,9 @@ function migrateSnapshot(data: { version: number; user: any }) {
96
99
  if (data.version < Versions.AddPasteAtCursor) {
97
100
  data.user.isPasteAtCursorMode = false
98
101
  }
102
+ if (data.version < Versions.AddKeyboardShortcuts) {
103
+ data.user.areKeyboardShortcutsEnabled = true
104
+ }
99
105
 
100
106
  // finally
101
107
  data.version = CURRENT_VERSION
@@ -139,6 +145,7 @@ export const defaultUserPreferences = Object.freeze({
139
145
  // N.B. These are duplicated in TLdrawAppUser.
140
146
  edgeScrollSpeed: 1,
141
147
  animationSpeed: userPrefersReducedMotion() ? 0 : 1,
148
+ areKeyboardShortcutsEnabled: true,
142
149
  isSnapMode: false,
143
150
  isWrapMode: false,
144
151
  isDynamicSizeMode: false,
@@ -425,3 +425,410 @@ describe('getShapesAtPoint', () => {
425
425
  expect(hollowShapesWithHitInside[0].id).toBe(ids.hollowShape)
426
426
  })
427
427
  })
428
+
429
+ describe('selectAll', () => {
430
+ const selectAllIds = {
431
+ pageShape1: createShapeId('pageShape1'),
432
+ pageShape2: createShapeId('pageShape2'),
433
+ pageShape3: createShapeId('pageShape3'),
434
+ container1: createShapeId('container1'),
435
+ containerChild1: createShapeId('containerChild1'),
436
+ containerChild2: createShapeId('containerChild2'),
437
+ containerChild3: createShapeId('containerChild3'),
438
+ containerGrandchild1: createShapeId('containerGrandchild1'),
439
+ container2: createShapeId('container2'),
440
+ container2Child1: createShapeId('container2Child1'),
441
+ container2Child2: createShapeId('container2Child2'),
442
+ container2Grandchild1: createShapeId('container2Grandchild1'),
443
+ lockedShape: createShapeId('lockedShape'),
444
+ }
445
+
446
+ beforeEach(() => {
447
+ // Clear any existing shapes
448
+ editor.selectAll().deleteShapes(editor.getSelectedShapeIds())
449
+
450
+ // Create shapes directly on the page (no parentId means they're children of the page)
451
+ editor.createShapes([
452
+ {
453
+ id: selectAllIds.pageShape1,
454
+ type: 'my-custom-shape',
455
+ x: 100,
456
+ y: 100,
457
+ props: { w: 100, h: 100 },
458
+ },
459
+ {
460
+ id: selectAllIds.pageShape2,
461
+ type: 'my-custom-shape',
462
+ x: 300,
463
+ y: 100,
464
+ props: { w: 100, h: 100 },
465
+ },
466
+ {
467
+ id: selectAllIds.pageShape3,
468
+ type: 'my-custom-shape',
469
+ x: 500,
470
+ y: 100,
471
+ props: { w: 100, h: 100 },
472
+ },
473
+ {
474
+ id: selectAllIds.lockedShape,
475
+ type: 'my-custom-shape',
476
+ x: 700,
477
+ y: 100,
478
+ props: { w: 100, h: 100 },
479
+ isLocked: true,
480
+ },
481
+ ])
482
+
483
+ // Create a container shape (simulating a frame or group)
484
+ editor.createShape({
485
+ id: selectAllIds.container1,
486
+ type: 'my-custom-shape',
487
+ x: 100,
488
+ y: 300,
489
+ props: { w: 400, h: 200 },
490
+ })
491
+
492
+ // Create children inside the container (parentId set to container1)
493
+ editor.createShapes([
494
+ {
495
+ id: selectAllIds.containerChild1,
496
+ type: 'my-custom-shape',
497
+ parentId: selectAllIds.container1,
498
+ x: 120,
499
+ y: 320,
500
+ props: { w: 50, h: 50 },
501
+ },
502
+ {
503
+ id: selectAllIds.containerChild2,
504
+ type: 'my-custom-shape',
505
+ parentId: selectAllIds.container1,
506
+ x: 200,
507
+ y: 320,
508
+ props: { w: 50, h: 50 },
509
+ },
510
+ {
511
+ id: selectAllIds.containerChild3,
512
+ type: 'my-custom-shape',
513
+ parentId: selectAllIds.container1,
514
+ x: 280,
515
+ y: 320,
516
+ props: { w: 50, h: 50 },
517
+ },
518
+ ])
519
+
520
+ // Create a grandchild inside one of the container children
521
+ editor.createShape({
522
+ id: selectAllIds.containerGrandchild1,
523
+ type: 'my-custom-shape',
524
+ parentId: selectAllIds.containerChild3,
525
+ x: 290,
526
+ y: 330,
527
+ props: { w: 30, h: 30 },
528
+ })
529
+
530
+ // Create a second container (simulating a group)
531
+ editor.createShape({
532
+ id: selectAllIds.container2,
533
+ type: 'my-custom-shape',
534
+ x: 600,
535
+ y: 300,
536
+ props: { w: 200, h: 200 },
537
+ })
538
+
539
+ // Create children inside the second container
540
+ editor.createShapes([
541
+ {
542
+ id: selectAllIds.container2Child1,
543
+ type: 'my-custom-shape',
544
+ parentId: selectAllIds.container2,
545
+ x: 620,
546
+ y: 320,
547
+ props: { w: 50, h: 50 },
548
+ },
549
+ {
550
+ id: selectAllIds.container2Child2,
551
+ type: 'my-custom-shape',
552
+ parentId: selectAllIds.container2,
553
+ x: 680,
554
+ y: 320,
555
+ props: { w: 50, h: 50 },
556
+ },
557
+ ])
558
+
559
+ // Create a grandchild in the second container
560
+ editor.createShape({
561
+ id: selectAllIds.container2Grandchild1,
562
+ type: 'my-custom-shape',
563
+ parentId: selectAllIds.container2Child1,
564
+ x: 630,
565
+ y: 330,
566
+ props: { w: 30, h: 30 },
567
+ })
568
+
569
+ // Clear selection
570
+ editor.selectNone()
571
+ })
572
+
573
+ it('when no shapes are selected, selects all page-level shapes (excluding locked ones)', () => {
574
+ // Initially no shapes selected
575
+ expect(editor.getSelectedShapeIds()).toEqual([])
576
+
577
+ // Call selectAll
578
+ editor.selectAll()
579
+
580
+ // Should select all page-level shapes (excluding locked ones)
581
+ const selectedIds = editor.getSelectedShapeIds()
582
+ expect(Array.from(selectedIds).sort()).toEqual(
583
+ [
584
+ selectAllIds.pageShape1,
585
+ selectAllIds.pageShape2,
586
+ selectAllIds.pageShape3,
587
+ selectAllIds.container1,
588
+ selectAllIds.container2,
589
+ ].sort()
590
+ )
591
+
592
+ // Should NOT include locked shape or children/grandchildren
593
+ expect(selectedIds).not.toContain(selectAllIds.lockedShape)
594
+ expect(selectedIds).not.toContain(selectAllIds.containerChild1)
595
+ expect(selectedIds).not.toContain(selectAllIds.containerChild2)
596
+ expect(selectedIds).not.toContain(selectAllIds.containerChild3)
597
+ expect(selectedIds).not.toContain(selectAllIds.containerGrandchild1)
598
+ expect(selectedIds).not.toContain(selectAllIds.container2Child1)
599
+ expect(selectedIds).not.toContain(selectAllIds.container2Child2)
600
+ expect(selectedIds).not.toContain(selectAllIds.container2Grandchild1)
601
+ })
602
+
603
+ it('when shapes are selected only on the page, all children of the page should be selected (but not their descendants)', () => {
604
+ // Select some page-level shapes
605
+ editor.select(selectAllIds.pageShape1, selectAllIds.pageShape2)
606
+
607
+ // Call selectAll
608
+ editor.selectAll()
609
+
610
+ // Should select all page-level shapes (excluding locked ones), but not descendants
611
+ const selectedIds = editor.getSelectedShapeIds()
612
+ expect(Array.from(selectedIds).sort()).toEqual(
613
+ [
614
+ selectAllIds.pageShape1,
615
+ selectAllIds.pageShape2,
616
+ selectAllIds.pageShape3,
617
+ selectAllIds.container1,
618
+ selectAllIds.container2,
619
+ ].sort()
620
+ )
621
+
622
+ // Should NOT include children or grandchildren or locked shapes
623
+ expect(selectedIds).not.toContain(selectAllIds.containerChild1)
624
+ expect(selectedIds).not.toContain(selectAllIds.containerChild2)
625
+ expect(selectedIds).not.toContain(selectAllIds.containerChild3)
626
+ expect(selectedIds).not.toContain(selectAllIds.containerGrandchild1)
627
+ expect(selectedIds).not.toContain(selectAllIds.container2Child1)
628
+ expect(selectedIds).not.toContain(selectAllIds.container2Child2)
629
+ expect(selectedIds).not.toContain(selectAllIds.container2Grandchild1)
630
+ expect(selectedIds).not.toContain(selectAllIds.lockedShape)
631
+ })
632
+
633
+ it('when shapes are selected within a container, only children of the container should be selected (not their descendants)', () => {
634
+ // Select some container children
635
+ editor.select(selectAllIds.containerChild1, selectAllIds.containerChild2)
636
+
637
+ // Call selectAll
638
+ editor.selectAll()
639
+
640
+ // Should select all container children (but not their descendants)
641
+ const selectedIds = editor.getSelectedShapeIds()
642
+ expect(Array.from(selectedIds).sort()).toEqual(
643
+ [
644
+ selectAllIds.containerChild1,
645
+ selectAllIds.containerChild2,
646
+ selectAllIds.containerChild3,
647
+ ].sort()
648
+ )
649
+
650
+ // Should NOT include page-level shapes or grandchildren
651
+ expect(selectedIds).not.toContain(selectAllIds.pageShape1)
652
+ expect(selectedIds).not.toContain(selectAllIds.pageShape2)
653
+ expect(selectedIds).not.toContain(selectAllIds.pageShape3)
654
+ expect(selectedIds).not.toContain(selectAllIds.container1)
655
+ expect(selectedIds).not.toContain(selectAllIds.container2)
656
+ expect(selectedIds).not.toContain(selectAllIds.containerGrandchild1)
657
+ expect(selectedIds).not.toContain(selectAllIds.container2Child1)
658
+ expect(selectedIds).not.toContain(selectAllIds.container2Child2)
659
+ expect(selectedIds).not.toContain(selectAllIds.container2Grandchild1)
660
+ })
661
+
662
+ it('when shapes are selected within a second container, only children of that container should be selected', () => {
663
+ // Select some second container children
664
+ editor.select(selectAllIds.container2Child1)
665
+
666
+ // Call selectAll
667
+ editor.selectAll()
668
+
669
+ // Should select all second container children (but not their descendants)
670
+ const selectedIds = editor.getSelectedShapeIds()
671
+ expect(Array.from(selectedIds).sort()).toEqual(
672
+ [selectAllIds.container2Child1, selectAllIds.container2Child2].sort()
673
+ )
674
+
675
+ // Should NOT include page-level shapes or other container's children or grandchildren
676
+ expect(selectedIds).not.toContain(selectAllIds.pageShape1)
677
+ expect(selectedIds).not.toContain(selectAllIds.pageShape2)
678
+ expect(selectedIds).not.toContain(selectAllIds.pageShape3)
679
+ expect(selectedIds).not.toContain(selectAllIds.container1)
680
+ expect(selectedIds).not.toContain(selectAllIds.container2)
681
+ expect(selectedIds).not.toContain(selectAllIds.containerChild1)
682
+ expect(selectedIds).not.toContain(selectAllIds.containerChild2)
683
+ expect(selectedIds).not.toContain(selectAllIds.containerChild3)
684
+ expect(selectedIds).not.toContain(selectAllIds.containerGrandchild1)
685
+ expect(selectedIds).not.toContain(selectAllIds.container2Grandchild1)
686
+ })
687
+
688
+ it('when shapes are selected that belong to different parents, no change/history entry should be made', () => {
689
+ // Select shapes from different parents (page and container)
690
+ editor.select(selectAllIds.pageShape1, selectAllIds.containerChild1)
691
+
692
+ const initialSelectedIds = editor.getSelectedShapeIds()
693
+
694
+ // Spy on setSelectedShapes to verify it's not called
695
+ const setSelectedShapesSpy = jest.spyOn(editor, 'setSelectedShapes')
696
+
697
+ // Call selectAll
698
+ editor.selectAll()
699
+
700
+ // Selection should remain unchanged
701
+ expect(editor.getSelectedShapeIds()).toEqual(initialSelectedIds)
702
+
703
+ // setSelectedShapes should not have been called (the method returns early)
704
+ expect(setSelectedShapesSpy).not.toHaveBeenCalled()
705
+
706
+ setSelectedShapesSpy.mockRestore()
707
+ })
708
+
709
+ it('when shapes are selected that belong to different containers, no change/history entry should be made', () => {
710
+ // Select shapes from different containers
711
+ editor.select(selectAllIds.containerChild1, selectAllIds.container2Child1)
712
+
713
+ const initialSelectedIds = editor.getSelectedShapeIds()
714
+
715
+ // Spy on setSelectedShapes to verify it's not called
716
+ const setSelectedShapesSpy = jest.spyOn(editor, 'setSelectedShapes')
717
+
718
+ // Call selectAll
719
+ editor.selectAll()
720
+
721
+ // Selection should remain unchanged
722
+ expect(editor.getSelectedShapeIds()).toEqual(initialSelectedIds)
723
+
724
+ // setSelectedShapes should not have been called
725
+ expect(setSelectedShapesSpy).not.toHaveBeenCalled()
726
+
727
+ setSelectedShapesSpy.mockRestore()
728
+ })
729
+
730
+ it('should not select locked shapes', () => {
731
+ // Select a page-level shape
732
+ editor.select(selectAllIds.pageShape1)
733
+
734
+ // Call selectAll
735
+ editor.selectAll()
736
+
737
+ // Should select all page-level shapes except locked ones
738
+ const selectedIds = editor.getSelectedShapeIds()
739
+ expect(selectedIds).not.toContain(selectAllIds.lockedShape)
740
+ expect(selectedIds).toContain(selectAllIds.pageShape1)
741
+ expect(selectedIds).toContain(selectAllIds.pageShape2)
742
+ expect(selectedIds).toContain(selectAllIds.pageShape3)
743
+ expect(selectedIds).toContain(selectAllIds.container1)
744
+ expect(selectedIds).toContain(selectAllIds.container2)
745
+ })
746
+
747
+ it('should handle empty container by selecting all siblings at the same level', () => {
748
+ // Create an empty container
749
+ const emptyContainerId = createShapeId('emptyContainer')
750
+ editor.createShape({
751
+ id: emptyContainerId,
752
+ type: 'my-custom-shape',
753
+ x: 800,
754
+ y: 400,
755
+ props: { w: 100, h: 100 },
756
+ })
757
+
758
+ // Clear selection first
759
+ editor.selectNone()
760
+
761
+ // Select the empty container
762
+ editor.select(emptyContainerId)
763
+
764
+ // Call selectAll - since the empty container has no children, it should select all siblings (page-level shapes)
765
+ editor.selectAll()
766
+
767
+ // Should select all page-level shapes (including the empty container itself)
768
+ const selectedIds = editor.getSelectedShapeIds()
769
+ expect(Array.from(selectedIds).sort()).toEqual(
770
+ [
771
+ selectAllIds.pageShape1,
772
+ selectAllIds.pageShape2,
773
+ selectAllIds.pageShape3,
774
+ selectAllIds.container1,
775
+ selectAllIds.container2,
776
+ emptyContainerId,
777
+ ].sort()
778
+ )
779
+
780
+ // Should NOT include locked shapes or children/grandchildren
781
+ expect(selectedIds).not.toContain(selectAllIds.lockedShape)
782
+ expect(selectedIds).not.toContain(selectAllIds.containerChild1)
783
+ expect(selectedIds).not.toContain(selectAllIds.containerChild2)
784
+ expect(selectedIds).not.toContain(selectAllIds.containerChild3)
785
+ expect(selectedIds).not.toContain(selectAllIds.containerGrandchild1)
786
+ expect(selectedIds).not.toContain(selectAllIds.container2Child1)
787
+ expect(selectedIds).not.toContain(selectAllIds.container2Child2)
788
+ expect(selectedIds).not.toContain(selectAllIds.container2Grandchild1)
789
+ })
790
+
791
+ it('should work correctly when selecting all shapes of same parent type', () => {
792
+ // Select all container children
793
+ editor.select(
794
+ selectAllIds.containerChild1,
795
+ selectAllIds.containerChild2,
796
+ selectAllIds.containerChild3
797
+ )
798
+
799
+ // Call selectAll - should maintain the same selection since all children are already selected
800
+ editor.selectAll()
801
+
802
+ // Should still have all container children selected
803
+ const selectedIds = editor.getSelectedShapeIds()
804
+ expect(Array.from(selectedIds).sort()).toEqual(
805
+ [
806
+ selectAllIds.containerChild1,
807
+ selectAllIds.containerChild2,
808
+ selectAllIds.containerChild3,
809
+ ].sort()
810
+ )
811
+ })
812
+
813
+ it('should handle mixed selection levels gracefully by doing nothing', () => {
814
+ // Select a mix: page shape (parent=page), container (parent=page), and container child (parent=container1)
815
+ // These all have different parent IDs so selectAll should do nothing
816
+ editor.select(selectAllIds.pageShape1, selectAllIds.containerChild1)
817
+
818
+ const initialSelectedIds = Array.from(editor.getSelectedShapeIds())
819
+
820
+ // Spy on setSelectedShapes to verify it's not called
821
+ const setSelectedShapesSpy = jest.spyOn(editor, 'setSelectedShapes')
822
+
823
+ // Call selectAll
824
+ editor.selectAll()
825
+
826
+ // Selection should remain unchanged since shapes have different parents
827
+ expect(Array.from(editor.getSelectedShapeIds())).toEqual(initialSelectedIds)
828
+
829
+ // setSelectedShapes should not have been called
830
+ expect(setSelectedShapesSpy).not.toHaveBeenCalled()
831
+
832
+ setSelectedShapesSpy.mockRestore()
833
+ })
834
+ })