@tldraw/editor 3.14.0-canary.f8af44c4d1e2 → 3.14.0-canary.ff61ab6deaa2
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.
- package/dist-cjs/index.d.ts +0 -17
- package/dist-cjs/index.js +8 -8
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/lib/editor/Editor.js +18 -52
- package/dist-cjs/lib/editor/Editor.js.map +2 -2
- package/dist-cjs/lib/editor/derivations/bindingsIndex.js +22 -22
- package/dist-cjs/lib/editor/derivations/bindingsIndex.js.map +2 -2
- package/dist-cjs/lib/editor/derivations/parentsToChildren.js +16 -16
- package/dist-cjs/lib/editor/derivations/parentsToChildren.js.map +2 -2
- package/dist-cjs/lib/editor/managers/{ClickManager.js → ClickManager/ClickManager.js} +1 -1
- package/dist-cjs/lib/editor/managers/ClickManager/ClickManager.js.map +7 -0
- package/dist-cjs/lib/editor/managers/{EdgeScrollManager.js → EdgeScrollManager/EdgeScrollManager.js} +2 -2
- package/dist-cjs/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.js.map +7 -0
- package/dist-cjs/lib/editor/managers/FocusManager/FocusManager.js.map +7 -0
- package/dist-cjs/lib/editor/managers/FontManager/FontManager.js.map +7 -0
- package/dist-cjs/lib/editor/managers/{HistoryManager.js → HistoryManager/HistoryManager.js} +64 -6
- package/dist-cjs/lib/editor/managers/HistoryManager/HistoryManager.js.map +7 -0
- package/dist-cjs/lib/editor/managers/{ScribbleManager.js → ScribbleManager/ScribbleManager.js} +1 -1
- package/dist-cjs/lib/editor/managers/ScribbleManager/ScribbleManager.js.map +7 -0
- package/dist-cjs/lib/editor/managers/TextManager/TextManager.js.map +7 -0
- package/dist-cjs/lib/editor/managers/{TickManager.js → TickManager/TickManager.js} +1 -1
- package/dist-cjs/lib/editor/managers/TickManager/TickManager.js.map +7 -0
- package/dist-cjs/lib/editor/managers/{UserPreferencesManager.js → UserPreferencesManager/UserPreferencesManager.js} +1 -1
- package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js.map +7 -0
- package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +1 -1
- package/dist-cjs/lib/exports/getSvgJsx.js.map +1 -1
- package/dist-cjs/lib/utils/reorderShapes.js +11 -10
- package/dist-cjs/lib/utils/reorderShapes.js.map +2 -2
- package/dist-cjs/lib/utils/richText.js.map +1 -1
- package/dist-cjs/version.js +3 -3
- package/dist-cjs/version.js.map +1 -1
- package/dist-esm/index.d.mts +0 -17
- package/dist-esm/index.mjs +12 -8
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/lib/editor/Editor.mjs +18 -52
- package/dist-esm/lib/editor/Editor.mjs.map +2 -2
- package/dist-esm/lib/editor/derivations/bindingsIndex.mjs +22 -22
- package/dist-esm/lib/editor/derivations/bindingsIndex.mjs.map +2 -2
- package/dist-esm/lib/editor/derivations/parentsToChildren.mjs +16 -16
- package/dist-esm/lib/editor/derivations/parentsToChildren.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/{ClickManager.mjs → ClickManager/ClickManager.mjs} +1 -1
- package/dist-esm/lib/editor/managers/ClickManager/ClickManager.mjs.map +7 -0
- package/dist-esm/lib/editor/managers/{EdgeScrollManager.mjs → EdgeScrollManager/EdgeScrollManager.mjs} +2 -2
- package/dist-esm/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.mjs.map +7 -0
- package/dist-esm/lib/editor/managers/FocusManager/FocusManager.mjs.map +7 -0
- package/dist-esm/lib/editor/managers/FontManager/FontManager.mjs.map +7 -0
- package/dist-esm/lib/editor/managers/{HistoryManager.mjs → HistoryManager/HistoryManager.mjs} +60 -2
- package/dist-esm/lib/editor/managers/HistoryManager/HistoryManager.mjs.map +7 -0
- package/dist-esm/lib/editor/managers/{ScribbleManager.mjs → ScribbleManager/ScribbleManager.mjs} +1 -1
- package/dist-esm/lib/editor/managers/ScribbleManager/ScribbleManager.mjs.map +7 -0
- package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs.map +7 -0
- package/dist-esm/lib/editor/managers/{TickManager.mjs → TickManager/TickManager.mjs} +1 -1
- package/dist-esm/lib/editor/managers/TickManager/TickManager.mjs.map +7 -0
- package/dist-esm/lib/editor/managers/{UserPreferencesManager.mjs → UserPreferencesManager/UserPreferencesManager.mjs} +1 -1
- package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs.map +7 -0
- package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +1 -1
- package/dist-esm/lib/exports/getSvgJsx.mjs.map +1 -1
- package/dist-esm/lib/utils/reorderShapes.mjs +11 -10
- package/dist-esm/lib/utils/reorderShapes.mjs.map +2 -2
- package/dist-esm/lib/utils/richText.mjs.map +1 -1
- package/dist-esm/version.mjs +3 -3
- package/dist-esm/version.mjs.map +1 -1
- package/package.json +7 -7
- package/src/index.ts +13 -7
- package/src/lib/editor/Editor.ts +20 -54
- package/src/lib/editor/derivations/bindingsIndex.ts +27 -26
- package/src/lib/editor/derivations/parentsToChildren.ts +28 -25
- package/src/lib/editor/managers/ClickManager/ClickManager.test.ts +442 -0
- package/src/lib/editor/managers/{ClickManager.ts → ClickManager/ClickManager.ts} +3 -3
- package/src/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.test.ts +374 -0
- package/src/lib/editor/managers/{EdgeScrollManager.ts → EdgeScrollManager/EdgeScrollManager.ts} +3 -3
- package/src/lib/editor/managers/FocusManager/FocusManager.test.ts +455 -0
- package/src/lib/editor/managers/{FocusManager.ts → FocusManager/FocusManager.ts} +1 -1
- package/src/lib/editor/managers/FontManager/FontManager.test.ts +263 -0
- package/src/lib/editor/managers/{FontManager.ts → FontManager/FontManager.ts} +1 -1
- package/src/lib/editor/managers/{HistoryManager.test.ts → HistoryManager/HistoryManager.test.ts} +388 -1
- package/src/lib/editor/managers/{HistoryManager.ts → HistoryManager/HistoryManager.ts} +73 -2
- package/src/lib/editor/managers/ScribbleManager/ScribbleManager.test.ts +624 -0
- package/src/lib/editor/managers/{ScribbleManager.ts → ScribbleManager/ScribbleManager.ts} +2 -2
- package/src/lib/editor/managers/SnapManager/SnapManager.test.ts +485 -0
- package/src/lib/editor/managers/TextManager/TextManager.test.ts +411 -0
- package/src/lib/editor/managers/{TextManager.ts → TextManager/TextManager.ts} +1 -1
- package/src/lib/editor/managers/TickManager/TickManager.test.ts +314 -0
- package/src/lib/editor/managers/{TickManager.ts → TickManager/TickManager.ts} +2 -2
- package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.test.ts +591 -0
- package/src/lib/editor/managers/{UserPreferencesManager.ts → UserPreferencesManager/UserPreferencesManager.ts} +2 -2
- package/src/lib/editor/shapes/ShapeUtil.ts +1 -1
- package/src/lib/exports/getSvgJsx.tsx +1 -1
- package/src/lib/utils/reorderShapes.ts +10 -13
- package/src/lib/utils/richText.ts +1 -1
- package/src/version.ts +3 -3
- package/dist-cjs/lib/editor/managers/ClickManager.js.map +0 -7
- package/dist-cjs/lib/editor/managers/EdgeScrollManager.js.map +0 -7
- package/dist-cjs/lib/editor/managers/FocusManager.js.map +0 -7
- package/dist-cjs/lib/editor/managers/FontManager.js.map +0 -7
- package/dist-cjs/lib/editor/managers/HistoryManager.js.map +0 -7
- package/dist-cjs/lib/editor/managers/ScribbleManager.js.map +0 -7
- package/dist-cjs/lib/editor/managers/Stack.js +0 -82
- package/dist-cjs/lib/editor/managers/Stack.js.map +0 -7
- package/dist-cjs/lib/editor/managers/TextManager.js.map +0 -7
- package/dist-cjs/lib/editor/managers/TickManager.js.map +0 -7
- package/dist-cjs/lib/editor/managers/UserPreferencesManager.js.map +0 -7
- package/dist-esm/lib/editor/managers/ClickManager.mjs.map +0 -7
- package/dist-esm/lib/editor/managers/EdgeScrollManager.mjs.map +0 -7
- package/dist-esm/lib/editor/managers/FocusManager.mjs.map +0 -7
- package/dist-esm/lib/editor/managers/FontManager.mjs.map +0 -7
- package/dist-esm/lib/editor/managers/HistoryManager.mjs.map +0 -7
- package/dist-esm/lib/editor/managers/ScribbleManager.mjs.map +0 -7
- package/dist-esm/lib/editor/managers/Stack.mjs +0 -62
- package/dist-esm/lib/editor/managers/Stack.mjs.map +0 -7
- package/dist-esm/lib/editor/managers/TextManager.mjs.map +0 -7
- package/dist-esm/lib/editor/managers/TickManager.mjs.map +0 -7
- package/dist-esm/lib/editor/managers/UserPreferencesManager.mjs.map +0 -7
- package/src/lib/editor/managers/ScribbleManager.test.ts +0 -32
- package/src/lib/editor/managers/Stack.ts +0 -71
- /package/dist-cjs/lib/editor/managers/{FocusManager.js → FocusManager/FocusManager.js} +0 -0
- /package/dist-cjs/lib/editor/managers/{FontManager.js → FontManager/FontManager.js} +0 -0
- /package/dist-cjs/lib/editor/managers/{TextManager.js → TextManager/TextManager.js} +0 -0
- /package/dist-esm/lib/editor/managers/{FocusManager.mjs → FocusManager/FocusManager.mjs} +0 -0
- /package/dist-esm/lib/editor/managers/{FontManager.mjs → FontManager/FontManager.mjs} +0 -0
- /package/dist-esm/lib/editor/managers/{TextManager.mjs → TextManager/TextManager.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
|
+
})
|