@tldraw/editor 3.14.0-canary.f8af44c4d1e2 → 3.14.0-canary.faba3f64c07f

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 (174) hide show
  1. package/dist-cjs/index.d.ts +150 -76
  2. package/dist-cjs/index.js +11 -10
  3. package/dist-cjs/index.js.map +2 -2
  4. package/dist-cjs/lib/config/TLSessionStateSnapshot.js +1 -12
  5. package/dist-cjs/lib/config/TLSessionStateSnapshot.js.map +3 -3
  6. package/dist-cjs/lib/editor/Editor.js +109 -94
  7. package/dist-cjs/lib/editor/Editor.js.map +2 -2
  8. package/dist-cjs/lib/editor/bindings/BindingUtil.js.map +2 -2
  9. package/dist-cjs/lib/editor/derivations/bindingsIndex.js +22 -22
  10. package/dist-cjs/lib/editor/derivations/bindingsIndex.js.map +2 -2
  11. package/dist-cjs/lib/editor/derivations/parentsToChildren.js +16 -16
  12. package/dist-cjs/lib/editor/derivations/parentsToChildren.js.map +2 -2
  13. package/dist-cjs/lib/editor/managers/{ClickManager.js → ClickManager/ClickManager.js} +1 -1
  14. package/dist-cjs/lib/editor/managers/ClickManager/ClickManager.js.map +7 -0
  15. package/dist-cjs/lib/editor/managers/{EdgeScrollManager.js → EdgeScrollManager/EdgeScrollManager.js} +2 -2
  16. package/dist-cjs/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.js.map +7 -0
  17. package/dist-cjs/lib/editor/managers/FocusManager/FocusManager.js.map +7 -0
  18. package/dist-cjs/lib/editor/managers/{FontManager.js → FontManager/FontManager.js} +1 -2
  19. package/dist-cjs/lib/editor/managers/FontManager/FontManager.js.map +7 -0
  20. package/dist-cjs/lib/editor/managers/{HistoryManager.js → HistoryManager/HistoryManager.js} +64 -6
  21. package/dist-cjs/lib/editor/managers/HistoryManager/HistoryManager.js.map +7 -0
  22. package/dist-cjs/lib/editor/managers/{ScribbleManager.js → ScribbleManager/ScribbleManager.js} +1 -1
  23. package/dist-cjs/lib/editor/managers/ScribbleManager/ScribbleManager.js.map +7 -0
  24. package/dist-cjs/lib/editor/managers/{TextManager.js → TextManager/TextManager.js} +73 -42
  25. package/dist-cjs/lib/editor/managers/TextManager/TextManager.js.map +7 -0
  26. package/dist-cjs/lib/editor/managers/{TickManager.js → TickManager/TickManager.js} +1 -1
  27. package/dist-cjs/lib/editor/managers/TickManager/TickManager.js.map +7 -0
  28. package/dist-cjs/lib/editor/managers/{UserPreferencesManager.js → UserPreferencesManager/UserPreferencesManager.js} +1 -1
  29. package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js.map +7 -0
  30. package/dist-cjs/lib/editor/shapes/ShapeUtil.js +0 -10
  31. package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +2 -2
  32. package/dist-cjs/lib/editor/shapes/group/GroupShapeUtil.js +1 -1
  33. package/dist-cjs/lib/editor/shapes/group/GroupShapeUtil.js.map +1 -1
  34. package/dist-cjs/lib/editor/tools/StateNode.js +3 -3
  35. package/dist-cjs/lib/editor/tools/StateNode.js.map +2 -2
  36. package/dist-cjs/lib/editor/types/emit-types.js.map +1 -1
  37. package/dist-cjs/lib/editor/types/external-content.js.map +1 -1
  38. package/dist-cjs/lib/exports/getSvgJsx.js.map +1 -1
  39. package/dist-cjs/lib/hooks/useCanvasEvents.js +1 -2
  40. package/dist-cjs/lib/hooks/useCanvasEvents.js.map +2 -2
  41. package/dist-cjs/lib/primitives/Box.js +0 -6
  42. package/dist-cjs/lib/primitives/Box.js.map +2 -2
  43. package/dist-cjs/lib/primitives/geometry/Geometry2d.js +6 -2
  44. package/dist-cjs/lib/primitives/geometry/Geometry2d.js.map +2 -2
  45. package/dist-cjs/lib/utils/areShapesContentEqual.js +1 -1
  46. package/dist-cjs/lib/utils/areShapesContentEqual.js.map +2 -2
  47. package/dist-cjs/lib/utils/reorderShapes.js +11 -10
  48. package/dist-cjs/lib/utils/reorderShapes.js.map +2 -2
  49. package/dist-cjs/lib/utils/reparenting.js +232 -0
  50. package/dist-cjs/lib/utils/reparenting.js.map +7 -0
  51. package/dist-cjs/lib/utils/richText.js +7 -2
  52. package/dist-cjs/lib/utils/richText.js.map +2 -2
  53. package/dist-cjs/version.js +3 -3
  54. package/dist-cjs/version.js.map +1 -1
  55. package/dist-esm/index.d.mts +150 -76
  56. package/dist-esm/index.mjs +15 -10
  57. package/dist-esm/index.mjs.map +2 -2
  58. package/dist-esm/lib/config/TLSessionStateSnapshot.mjs +1 -1
  59. package/dist-esm/lib/config/TLSessionStateSnapshot.mjs.map +2 -2
  60. package/dist-esm/lib/editor/Editor.mjs +109 -94
  61. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  62. package/dist-esm/lib/editor/bindings/BindingUtil.mjs.map +2 -2
  63. package/dist-esm/lib/editor/derivations/bindingsIndex.mjs +22 -22
  64. package/dist-esm/lib/editor/derivations/bindingsIndex.mjs.map +2 -2
  65. package/dist-esm/lib/editor/derivations/parentsToChildren.mjs +16 -16
  66. package/dist-esm/lib/editor/derivations/parentsToChildren.mjs.map +2 -2
  67. package/dist-esm/lib/editor/managers/{ClickManager.mjs → ClickManager/ClickManager.mjs} +1 -1
  68. package/dist-esm/lib/editor/managers/ClickManager/ClickManager.mjs.map +7 -0
  69. package/dist-esm/lib/editor/managers/{EdgeScrollManager.mjs → EdgeScrollManager/EdgeScrollManager.mjs} +2 -2
  70. package/dist-esm/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.mjs.map +7 -0
  71. package/dist-esm/lib/editor/managers/FocusManager/FocusManager.mjs.map +7 -0
  72. package/dist-esm/lib/editor/managers/{FontManager.mjs → FontManager/FontManager.mjs} +1 -2
  73. package/dist-esm/lib/editor/managers/FontManager/FontManager.mjs.map +7 -0
  74. package/dist-esm/lib/editor/managers/{HistoryManager.mjs → HistoryManager/HistoryManager.mjs} +60 -2
  75. package/dist-esm/lib/editor/managers/HistoryManager/HistoryManager.mjs.map +7 -0
  76. package/dist-esm/lib/editor/managers/{ScribbleManager.mjs → ScribbleManager/ScribbleManager.mjs} +1 -1
  77. package/dist-esm/lib/editor/managers/ScribbleManager/ScribbleManager.mjs.map +7 -0
  78. package/dist-esm/lib/editor/managers/{TextManager.mjs → TextManager/TextManager.mjs} +73 -42
  79. package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs.map +7 -0
  80. package/dist-esm/lib/editor/managers/{TickManager.mjs → TickManager/TickManager.mjs} +1 -1
  81. package/dist-esm/lib/editor/managers/TickManager/TickManager.mjs.map +7 -0
  82. package/dist-esm/lib/editor/managers/{UserPreferencesManager.mjs → UserPreferencesManager/UserPreferencesManager.mjs} +1 -1
  83. package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs.map +7 -0
  84. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs +0 -10
  85. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +2 -2
  86. package/dist-esm/lib/editor/shapes/group/GroupShapeUtil.mjs +1 -1
  87. package/dist-esm/lib/editor/shapes/group/GroupShapeUtil.mjs.map +1 -1
  88. package/dist-esm/lib/editor/tools/StateNode.mjs +3 -3
  89. package/dist-esm/lib/editor/tools/StateNode.mjs.map +2 -2
  90. package/dist-esm/lib/exports/getSvgJsx.mjs.map +1 -1
  91. package/dist-esm/lib/hooks/useCanvasEvents.mjs +1 -2
  92. package/dist-esm/lib/hooks/useCanvasEvents.mjs.map +2 -2
  93. package/dist-esm/lib/primitives/Box.mjs +0 -6
  94. package/dist-esm/lib/primitives/Box.mjs.map +2 -2
  95. package/dist-esm/lib/primitives/geometry/Geometry2d.mjs +6 -2
  96. package/dist-esm/lib/primitives/geometry/Geometry2d.mjs.map +2 -2
  97. package/dist-esm/lib/utils/areShapesContentEqual.mjs +1 -1
  98. package/dist-esm/lib/utils/areShapesContentEqual.mjs.map +2 -2
  99. package/dist-esm/lib/utils/reorderShapes.mjs +11 -10
  100. package/dist-esm/lib/utils/reorderShapes.mjs.map +2 -2
  101. package/dist-esm/lib/utils/reparenting.mjs +216 -0
  102. package/dist-esm/lib/utils/reparenting.mjs.map +7 -0
  103. package/dist-esm/lib/utils/richText.mjs +8 -3
  104. package/dist-esm/lib/utils/richText.mjs.map +2 -2
  105. package/dist-esm/version.mjs +3 -3
  106. package/dist-esm/version.mjs.map +1 -1
  107. package/editor.css +442 -492
  108. package/package.json +8 -9
  109. package/src/index.ts +20 -8
  110. package/src/lib/config/TLSessionStateSnapshot.ts +1 -1
  111. package/src/lib/editor/Editor.test.ts +252 -3
  112. package/src/lib/editor/Editor.ts +120 -101
  113. package/src/lib/editor/bindings/BindingUtil.ts +6 -0
  114. package/src/lib/editor/derivations/bindingsIndex.ts +27 -26
  115. package/src/lib/editor/derivations/parentsToChildren.ts +28 -25
  116. package/src/lib/editor/managers/ClickManager/ClickManager.test.ts +442 -0
  117. package/src/lib/editor/managers/{ClickManager.ts → ClickManager/ClickManager.ts} +3 -3
  118. package/src/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.test.ts +374 -0
  119. package/src/lib/editor/managers/{EdgeScrollManager.ts → EdgeScrollManager/EdgeScrollManager.ts} +3 -3
  120. package/src/lib/editor/managers/FocusManager/FocusManager.test.ts +455 -0
  121. package/src/lib/editor/managers/{FocusManager.ts → FocusManager/FocusManager.ts} +1 -1
  122. package/src/lib/editor/managers/FontManager/FontManager.test.ts +263 -0
  123. package/src/lib/editor/managers/{FontManager.ts → FontManager/FontManager.ts} +2 -3
  124. package/src/lib/editor/managers/{HistoryManager.test.ts → HistoryManager/HistoryManager.test.ts} +388 -1
  125. package/src/lib/editor/managers/{HistoryManager.ts → HistoryManager/HistoryManager.ts} +73 -2
  126. package/src/lib/editor/managers/ScribbleManager/ScribbleManager.test.ts +624 -0
  127. package/src/lib/editor/managers/{ScribbleManager.ts → ScribbleManager/ScribbleManager.ts} +2 -2
  128. package/src/lib/editor/managers/SnapManager/SnapManager.test.ts +485 -0
  129. package/src/lib/editor/managers/TextManager/TextManager.test.ts +407 -0
  130. package/src/lib/editor/managers/{TextManager.ts → TextManager/TextManager.ts} +119 -87
  131. package/src/lib/editor/managers/TickManager/TickManager.test.ts +314 -0
  132. package/src/lib/editor/managers/{TickManager.ts → TickManager/TickManager.ts} +2 -2
  133. package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.test.ts +591 -0
  134. package/src/lib/editor/managers/{UserPreferencesManager.ts → UserPreferencesManager/UserPreferencesManager.ts} +2 -2
  135. package/src/lib/editor/shapes/ShapeUtil.ts +48 -16
  136. package/src/lib/editor/shapes/group/GroupShapeUtil.tsx +1 -1
  137. package/src/lib/editor/tools/StateNode.ts +3 -3
  138. package/src/lib/editor/types/emit-types.ts +4 -0
  139. package/src/lib/editor/types/external-content.ts +11 -2
  140. package/src/lib/exports/getSvgJsx.tsx +1 -1
  141. package/src/lib/hooks/useCanvasEvents.ts +0 -1
  142. package/src/lib/primitives/Box.ts +0 -8
  143. package/src/lib/primitives/geometry/Geometry2d.ts +7 -2
  144. package/src/lib/utils/areShapesContentEqual.ts +1 -2
  145. package/src/lib/utils/reorderShapes.ts +10 -13
  146. package/src/lib/utils/reparenting.ts +383 -0
  147. package/src/lib/utils/richText.ts +10 -4
  148. package/src/version.ts +3 -3
  149. package/dist-cjs/lib/editor/managers/ClickManager.js.map +0 -7
  150. package/dist-cjs/lib/editor/managers/EdgeScrollManager.js.map +0 -7
  151. package/dist-cjs/lib/editor/managers/FocusManager.js.map +0 -7
  152. package/dist-cjs/lib/editor/managers/FontManager.js.map +0 -7
  153. package/dist-cjs/lib/editor/managers/HistoryManager.js.map +0 -7
  154. package/dist-cjs/lib/editor/managers/ScribbleManager.js.map +0 -7
  155. package/dist-cjs/lib/editor/managers/Stack.js +0 -82
  156. package/dist-cjs/lib/editor/managers/Stack.js.map +0 -7
  157. package/dist-cjs/lib/editor/managers/TextManager.js.map +0 -7
  158. package/dist-cjs/lib/editor/managers/TickManager.js.map +0 -7
  159. package/dist-cjs/lib/editor/managers/UserPreferencesManager.js.map +0 -7
  160. package/dist-esm/lib/editor/managers/ClickManager.mjs.map +0 -7
  161. package/dist-esm/lib/editor/managers/EdgeScrollManager.mjs.map +0 -7
  162. package/dist-esm/lib/editor/managers/FocusManager.mjs.map +0 -7
  163. package/dist-esm/lib/editor/managers/FontManager.mjs.map +0 -7
  164. package/dist-esm/lib/editor/managers/HistoryManager.mjs.map +0 -7
  165. package/dist-esm/lib/editor/managers/ScribbleManager.mjs.map +0 -7
  166. package/dist-esm/lib/editor/managers/Stack.mjs +0 -62
  167. package/dist-esm/lib/editor/managers/Stack.mjs.map +0 -7
  168. package/dist-esm/lib/editor/managers/TextManager.mjs.map +0 -7
  169. package/dist-esm/lib/editor/managers/TickManager.mjs.map +0 -7
  170. package/dist-esm/lib/editor/managers/UserPreferencesManager.mjs.map +0 -7
  171. package/src/lib/editor/managers/ScribbleManager.test.ts +0 -32
  172. package/src/lib/editor/managers/Stack.ts +0 -71
  173. /package/dist-cjs/lib/editor/managers/{FocusManager.js → FocusManager/FocusManager.js} +0 -0
  174. /package/dist-esm/lib/editor/managers/{FocusManager.mjs → FocusManager/FocusManager.mjs} +0 -0
@@ -0,0 +1,455 @@
1
+ import { Editor } from '../../Editor'
2
+ import { FocusManager } from './FocusManager'
3
+
4
+ // Mock the Editor class
5
+ jest.mock('../../Editor')
6
+
7
+ describe('FocusManager', () => {
8
+ let editor: jest.Mocked<
9
+ Editor & {
10
+ sideEffects: {
11
+ registerAfterChangeHandler: jest.Mock
12
+ }
13
+ getInstanceState: jest.Mock
14
+ updateInstanceState: jest.Mock
15
+ getContainer: jest.Mock
16
+ isIn: jest.Mock
17
+ getSelectedShapeIds: jest.Mock
18
+ complete: jest.Mock
19
+ }
20
+ >
21
+ let focusManager: FocusManager
22
+ let mockContainer: HTMLElement
23
+ let mockDispose: jest.Mock
24
+ let originalAddEventListener: typeof document.body.addEventListener
25
+ let originalRemoveEventListener: typeof document.body.removeEventListener
26
+
27
+ beforeEach(() => {
28
+ // Create mock container element
29
+ mockContainer = document.createElement('div')
30
+ mockContainer.focus = jest.fn()
31
+ mockContainer.blur = jest.fn()
32
+ jest.spyOn(mockContainer.classList, 'add')
33
+ jest.spyOn(mockContainer.classList, 'remove')
34
+
35
+ // Create mock dispose function
36
+ mockDispose = jest.fn()
37
+
38
+ // Mock editor
39
+ editor = {
40
+ sideEffects: {
41
+ registerAfterChangeHandler: jest.fn(() => mockDispose),
42
+ },
43
+ getInstanceState: jest.fn(() => ({ isFocused: false })),
44
+ updateInstanceState: jest.fn(),
45
+ getContainer: jest.fn(() => mockContainer),
46
+ isIn: jest.fn(() => false),
47
+ getSelectedShapeIds: jest.fn(() => []),
48
+ complete: jest.fn(),
49
+ } as any
50
+
51
+ // Mock document.body event listeners
52
+ originalAddEventListener = document.body.addEventListener
53
+ originalRemoveEventListener = document.body.removeEventListener
54
+ document.body.addEventListener = jest.fn()
55
+ document.body.removeEventListener = jest.fn()
56
+ })
57
+
58
+ afterEach(() => {
59
+ // Restore original event listeners
60
+ document.body.addEventListener = originalAddEventListener
61
+ document.body.removeEventListener = originalRemoveEventListener
62
+
63
+ // Clean up any existing focus manager
64
+ if (focusManager) {
65
+ focusManager.dispose()
66
+ }
67
+
68
+ jest.clearAllMocks()
69
+ })
70
+
71
+ describe('constructor', () => {
72
+ it('should initialize with editor reference', () => {
73
+ focusManager = new FocusManager(editor)
74
+ expect(focusManager.editor).toBe(editor)
75
+ })
76
+
77
+ it('should register side effect listener for instance state changes', () => {
78
+ focusManager = new FocusManager(editor)
79
+ expect(editor.sideEffects.registerAfterChangeHandler).toHaveBeenCalledWith(
80
+ 'instance',
81
+ expect.any(Function)
82
+ )
83
+ })
84
+
85
+ it('should update container class on initialization', () => {
86
+ focusManager = new FocusManager(editor)
87
+ expect(mockContainer.classList.add).toHaveBeenCalledWith('tl-container__no-focus-ring')
88
+ })
89
+
90
+ it('should set up keyboard event listener', () => {
91
+ focusManager = new FocusManager(editor)
92
+ expect(document.body.addEventListener).toHaveBeenCalledWith('keydown', expect.any(Function))
93
+ })
94
+
95
+ it('should set up mouse event listener', () => {
96
+ focusManager = new FocusManager(editor)
97
+ expect(document.body.addEventListener).toHaveBeenCalledWith('mousedown', expect.any(Function))
98
+ })
99
+
100
+ it('should set focus state to true when autoFocus is true', () => {
101
+ editor.getInstanceState.mockReturnValue({ isFocused: false })
102
+ focusManager = new FocusManager(editor, true)
103
+ expect(editor.updateInstanceState).toHaveBeenCalledWith({ isFocused: true })
104
+ })
105
+
106
+ it('should set focus state to false when autoFocus is false', () => {
107
+ editor.getInstanceState.mockReturnValue({ isFocused: true })
108
+ focusManager = new FocusManager(editor, false)
109
+ expect(editor.updateInstanceState).toHaveBeenCalledWith({ isFocused: false })
110
+ })
111
+
112
+ it('should not change focus state when autoFocus matches current state', () => {
113
+ editor.getInstanceState.mockReturnValue({ isFocused: true })
114
+ focusManager = new FocusManager(editor, true)
115
+ expect(editor.updateInstanceState).not.toHaveBeenCalled()
116
+ })
117
+
118
+ it('should handle undefined autoFocus parameter', () => {
119
+ editor.getInstanceState.mockReturnValue({ isFocused: true })
120
+ focusManager = new FocusManager(editor)
121
+ expect(editor.updateInstanceState).toHaveBeenCalledWith({ isFocused: false })
122
+ })
123
+ })
124
+
125
+ describe('side effect handler', () => {
126
+ it('should update container class when focus state changes', () => {
127
+ focusManager = new FocusManager(editor)
128
+
129
+ // Get the registered handler function
130
+ const handlerCall = editor.sideEffects.registerAfterChangeHandler.mock.calls[0]
131
+ const handler = handlerCall[1]
132
+
133
+ // Clear previous calls
134
+ jest.clearAllMocks()
135
+
136
+ // Simulate focus state change
137
+ const prev = { isFocused: false }
138
+ const next = { isFocused: true }
139
+ editor.getInstanceState.mockReturnValue(next)
140
+
141
+ handler(prev, next)
142
+
143
+ expect(mockContainer.classList.add).toHaveBeenCalledWith('tl-container__focused')
144
+ })
145
+
146
+ it('should not update container class when focus state does not change', () => {
147
+ focusManager = new FocusManager(editor)
148
+
149
+ const handlerCall = editor.sideEffects.registerAfterChangeHandler.mock.calls[0]
150
+ const handler = handlerCall[1]
151
+
152
+ jest.clearAllMocks()
153
+
154
+ // Simulate no focus state change
155
+ const prev = { isFocused: true }
156
+ const next = { isFocused: true }
157
+
158
+ handler(prev, next)
159
+
160
+ expect(mockContainer.classList.add).not.toHaveBeenCalled()
161
+ expect(mockContainer.classList.remove).not.toHaveBeenCalled()
162
+ })
163
+ })
164
+
165
+ describe('updateContainerClass', () => {
166
+ let handler: (prev: any, next: any) => void
167
+
168
+ beforeEach(() => {
169
+ focusManager = new FocusManager(editor)
170
+ // Get the handler before clearing mocks
171
+ const handlerCall = editor.sideEffects.registerAfterChangeHandler.mock.calls[0]
172
+ handler = handlerCall[1]
173
+ jest.clearAllMocks()
174
+ })
175
+
176
+ it('should add focused class when editor is focused', () => {
177
+ editor.getInstanceState.mockReturnValue({ isFocused: true })
178
+
179
+ handler({ isFocused: false }, { isFocused: true })
180
+
181
+ expect(mockContainer.classList.add).toHaveBeenCalledWith('tl-container__focused')
182
+ })
183
+
184
+ it('should remove focused class when editor is not focused', () => {
185
+ editor.getInstanceState.mockReturnValue({ isFocused: false })
186
+
187
+ handler({ isFocused: true }, { isFocused: false })
188
+
189
+ expect(mockContainer.classList.remove).toHaveBeenCalledWith('tl-container__focused')
190
+ })
191
+
192
+ it('should always add no-focus-ring class', () => {
193
+ editor.getInstanceState.mockReturnValue({ isFocused: true })
194
+
195
+ handler({ isFocused: false }, { isFocused: true })
196
+
197
+ expect(mockContainer.classList.add).toHaveBeenCalledWith('tl-container__no-focus-ring')
198
+ })
199
+ })
200
+
201
+ describe('handleKeyDown', () => {
202
+ let keydownHandler: (event: KeyboardEvent) => void
203
+
204
+ beforeEach(() => {
205
+ focusManager = new FocusManager(editor)
206
+
207
+ // Get the keydown handler that was registered
208
+ const addEventListenerCalls = (document.body.addEventListener as jest.Mock).mock.calls
209
+ const keydownCall = addEventListenerCalls.find((call) => call[0] === 'keydown')
210
+ keydownHandler = keydownCall[1]
211
+
212
+ jest.clearAllMocks()
213
+ })
214
+
215
+ it('should remove no-focus-ring class on Tab key', () => {
216
+ const event = new KeyboardEvent('keydown', { key: 'Tab' })
217
+ keydownHandler(event)
218
+
219
+ expect(mockContainer.classList.remove).toHaveBeenCalledWith('tl-container__no-focus-ring')
220
+ })
221
+
222
+ it('should remove no-focus-ring class on ArrowUp key', () => {
223
+ const event = new KeyboardEvent('keydown', { key: 'ArrowUp' })
224
+ keydownHandler(event)
225
+
226
+ expect(mockContainer.classList.remove).toHaveBeenCalledWith('tl-container__no-focus-ring')
227
+ })
228
+
229
+ it('should remove no-focus-ring class on ArrowDown key', () => {
230
+ const event = new KeyboardEvent('keydown', { key: 'ArrowDown' })
231
+ keydownHandler(event)
232
+
233
+ expect(mockContainer.classList.remove).toHaveBeenCalledWith('tl-container__no-focus-ring')
234
+ })
235
+
236
+ it('should not remove no-focus-ring class on other keys', () => {
237
+ const event = new KeyboardEvent('keydown', { key: 'Enter' })
238
+ keydownHandler(event)
239
+
240
+ expect(mockContainer.classList.remove).not.toHaveBeenCalled()
241
+ })
242
+
243
+ it('should return early when editor is in editing mode', () => {
244
+ editor.isIn.mockReturnValue(true)
245
+ const event = new KeyboardEvent('keydown', { key: 'Tab' })
246
+
247
+ keydownHandler(event)
248
+
249
+ expect(mockContainer.classList.remove).not.toHaveBeenCalled()
250
+ })
251
+
252
+ it('should return early when container is active element and shapes are selected', () => {
253
+ Object.defineProperty(document, 'activeElement', {
254
+ value: mockContainer,
255
+ configurable: true,
256
+ })
257
+ editor.getSelectedShapeIds.mockReturnValue(['shape1'])
258
+
259
+ const event = new KeyboardEvent('keydown', { key: 'Tab' })
260
+ keydownHandler(event)
261
+
262
+ expect(mockContainer.classList.remove).not.toHaveBeenCalled()
263
+ })
264
+
265
+ it('should process key when container is active but no shapes selected', () => {
266
+ Object.defineProperty(document, 'activeElement', {
267
+ value: mockContainer,
268
+ configurable: true,
269
+ })
270
+ editor.getSelectedShapeIds.mockReturnValue([])
271
+
272
+ const event = new KeyboardEvent('keydown', { key: 'Tab' })
273
+ keydownHandler(event)
274
+
275
+ expect(mockContainer.classList.remove).toHaveBeenCalledWith('tl-container__no-focus-ring')
276
+ })
277
+ })
278
+
279
+ describe('handleMouseDown', () => {
280
+ let mousedownHandler: () => void
281
+
282
+ beforeEach(() => {
283
+ focusManager = new FocusManager(editor)
284
+
285
+ // Get the mousedown handler that was registered
286
+ const addEventListenerCalls = (document.body.addEventListener as jest.Mock).mock.calls
287
+ const mousedownCall = addEventListenerCalls.find((call) => call[0] === 'mousedown')
288
+ mousedownHandler = mousedownCall[1]
289
+
290
+ jest.clearAllMocks()
291
+ })
292
+
293
+ it('should add no-focus-ring class on mouse down', () => {
294
+ mousedownHandler()
295
+
296
+ expect(mockContainer.classList.add).toHaveBeenCalledWith('tl-container__no-focus-ring')
297
+ })
298
+ })
299
+
300
+ describe('focus', () => {
301
+ beforeEach(() => {
302
+ focusManager = new FocusManager(editor)
303
+ })
304
+
305
+ it('should focus the container', () => {
306
+ focusManager.focus()
307
+ expect(mockContainer.focus).toHaveBeenCalled()
308
+ })
309
+ })
310
+
311
+ describe('blur', () => {
312
+ beforeEach(() => {
313
+ focusManager = new FocusManager(editor)
314
+ })
315
+
316
+ it('should complete editor interactions', () => {
317
+ focusManager.blur()
318
+ expect(editor.complete).toHaveBeenCalled()
319
+ })
320
+
321
+ it('should blur the container', () => {
322
+ focusManager.blur()
323
+ expect(mockContainer.blur).toHaveBeenCalled()
324
+ })
325
+
326
+ it('should complete before bluring', () => {
327
+ const callOrder: string[] = []
328
+ editor.complete.mockImplementation(() => callOrder.push('complete'))
329
+ mockContainer.blur = jest.fn(() => callOrder.push('blur'))
330
+
331
+ focusManager.blur()
332
+
333
+ expect(callOrder).toEqual(['complete', 'blur'])
334
+ })
335
+ })
336
+
337
+ describe('dispose', () => {
338
+ beforeEach(() => {
339
+ focusManager = new FocusManager(editor)
340
+ jest.clearAllMocks()
341
+ })
342
+
343
+ it('should remove keyboard event listener', () => {
344
+ focusManager.dispose()
345
+ expect(document.body.removeEventListener).toHaveBeenCalledWith(
346
+ 'keydown',
347
+ expect.any(Function)
348
+ )
349
+ })
350
+
351
+ it('should remove mouse event listener', () => {
352
+ focusManager.dispose()
353
+ expect(document.body.removeEventListener).toHaveBeenCalledWith(
354
+ 'mousedown',
355
+ expect.any(Function)
356
+ )
357
+ })
358
+
359
+ it('should dispose side effect listener', () => {
360
+ focusManager.dispose()
361
+ expect(mockDispose).toHaveBeenCalled()
362
+ })
363
+
364
+ it('should handle missing side effect disposal gracefully', () => {
365
+ // Create a focus manager where the side effect registration returns undefined
366
+ editor.sideEffects.registerAfterChangeHandler.mockReturnValue(undefined)
367
+ const focusManagerWithoutDispose = new FocusManager(editor)
368
+
369
+ expect(() => focusManagerWithoutDispose.dispose()).not.toThrow()
370
+ })
371
+ })
372
+
373
+ describe('integration scenarios', () => {
374
+ it('should handle rapid focus state changes', () => {
375
+ focusManager = new FocusManager(editor)
376
+ const handlerCall = editor.sideEffects.registerAfterChangeHandler.mock.calls[0]
377
+ const handler = handlerCall[1]
378
+
379
+ jest.clearAllMocks()
380
+
381
+ // Rapid focus changes
382
+ editor.getInstanceState.mockReturnValue({ isFocused: true })
383
+ handler({ isFocused: false }, { isFocused: true })
384
+
385
+ editor.getInstanceState.mockReturnValue({ isFocused: false })
386
+ handler({ isFocused: true }, { isFocused: false })
387
+
388
+ editor.getInstanceState.mockReturnValue({ isFocused: true })
389
+ handler({ isFocused: false }, { isFocused: true })
390
+
391
+ expect(mockContainer.classList.add).toHaveBeenCalledWith('tl-container__focused')
392
+ expect(mockContainer.classList.remove).toHaveBeenCalledWith('tl-container__focused')
393
+ })
394
+
395
+ it('should handle keyboard navigation while editing', () => {
396
+ focusManager = new FocusManager(editor)
397
+ const addEventListenerCalls = (document.body.addEventListener as jest.Mock).mock.calls
398
+ const keydownCall = addEventListenerCalls.find((call) => call[0] === 'keydown')
399
+ const keydownHandler = keydownCall[1]
400
+
401
+ editor.isIn.mockReturnValue(true) // Editing mode
402
+
403
+ const event = new KeyboardEvent('keydown', { key: 'Tab' })
404
+ keydownHandler(event)
405
+
406
+ // Should not remove focus ring when editing
407
+ expect(mockContainer.classList.remove).not.toHaveBeenCalledWith('tl-container__no-focus-ring')
408
+ })
409
+
410
+ it('should handle mouse and keyboard interaction sequence', () => {
411
+ focusManager = new FocusManager(editor)
412
+ const addEventListenerCalls = (document.body.addEventListener as jest.Mock).mock.calls
413
+
414
+ const mousedownCall = addEventListenerCalls.find((call) => call[0] === 'mousedown')
415
+ const keydownCall = addEventListenerCalls.find((call) => call[0] === 'keydown')
416
+
417
+ const mousedownHandler = mousedownCall[1]
418
+ const keydownHandler = keydownCall[1]
419
+
420
+ jest.clearAllMocks()
421
+
422
+ // Mouse down adds no-focus-ring
423
+ mousedownHandler()
424
+ expect(mockContainer.classList.add).toHaveBeenCalledWith('tl-container__no-focus-ring')
425
+
426
+ // Keyboard navigation removes no-focus-ring
427
+ const event = new KeyboardEvent('keydown', { key: 'Tab' })
428
+ keydownHandler(event)
429
+ expect(mockContainer.classList.remove).toHaveBeenCalledWith('tl-container__no-focus-ring')
430
+ })
431
+ })
432
+
433
+ describe('edge cases', () => {
434
+ it('should handle container being null', () => {
435
+ editor.getContainer.mockReturnValue(null as any)
436
+
437
+ expect(() => new FocusManager(editor)).toThrow()
438
+ })
439
+
440
+ it('should handle missing instance state', () => {
441
+ editor.getInstanceState.mockReturnValue(null as any)
442
+
443
+ expect(() => new FocusManager(editor)).toThrow()
444
+ })
445
+
446
+ it('should handle disposed manager gracefully', () => {
447
+ focusManager = new FocusManager(editor)
448
+ focusManager.dispose()
449
+
450
+ // Should not throw when calling methods after disposal
451
+ expect(() => focusManager.focus()).not.toThrow()
452
+ expect(() => focusManager.blur()).not.toThrow()
453
+ })
454
+ })
455
+ })
@@ -1,4 +1,4 @@
1
- import type { Editor } from '../Editor'
1
+ import type { Editor } from '../../Editor'
2
2
 
3
3
  /**
4
4
  * A manager for ensuring correct focus across the editor.