Skip to content

Commit eb96c4b

Browse files
committed
fix: issues with expanding selection down using Shift+ArrowDown
1 parent b66cb39 commit eb96c4b

File tree

4 files changed

+327
-14
lines changed

4 files changed

+327
-14
lines changed

.changeset/bumpy-spoons-check.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@portabletext/editor': patch
3+
---
4+
5+
fix: issues with expanding selection down using Shift+ArrowDown

packages/editor/gherkin-spec/selection.feature

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,41 @@ Feature: Selection
33
Background:
44
Given one editor
55

6+
Scenario Outline: Expanding selection down
7+
Given the text <text>
8+
When the caret is put <position>
9+
And "{Shift>}{ArrowDown}{/Shift}" is pressed
10+
And "{Shift>}{ArrowDown}{/Shift}" is pressed
11+
Then <selection> is selected
12+
13+
Examples:
14+
| text | position | selection |
15+
| "foo\|bar\|baz" | before "foo" | "foo\|bar\|" |
16+
| "foo\|>#:bar\|baz" | before "foo" | "foo\|>#:bar\|" |
17+
| "foo\|>#:bar\|{image}" | before "foo" | "foo\|>#:bar" |
18+
19+
Scenario Outline: Expanding selection down into block object
20+
Given the text "foo|{image}|bar"
21+
When the caret is put before "foo"
22+
And "{Shift>}{ArrowDown}{/Shift}" is pressed
23+
And "{Shift>}{ArrowDown}{/Shift}" is pressed
24+
Then "foo|{image}" is selected
25+
26+
Scenario Outline: Expanding selection down through block objects
27+
Given the text <text>
28+
When the caret is put before "foo"
29+
And "{Shift>}{ArrowDown}{/Shift}" is pressed
30+
And "{Shift>}{ArrowDown}{/Shift}" is pressed
31+
And "{Shift>}{ArrowDown}{/Shift}" is pressed
32+
Then <selection> is selected
33+
34+
Examples:
35+
| text | selection |
36+
| "foo\|{image}\|bar" | "foo\|{image}\|bar" |
37+
| "foo\|{image}\|bar\|baz" | "foo\|{image}\|bar\|" |
38+
| "foo\|{image}\|bar\|{image}" | "foo\|{image}\|bar" |
39+
| "foo\|{image}\|{image}\|bar" | "foo\|{image}\|{image}" |
40+
641
Scenario: Expanding collapsed selection backwards from empty line
742
Given the text "foo|"
843
And the editor is focused

packages/editor/src/behaviors/behavior.abstract.keyboard.ts

Lines changed: 265 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,20 @@
1-
import {createKeyboardShortcut} from '@portabletext/keyboard-shortcuts'
21
import {isTextBlock} from '@portabletext/schema'
32
import {defaultKeyboardShortcuts} from '../keyboard-shortcuts/default-keyboard-shortcuts'
3+
import {getFocusBlockObject} from '../selectors'
44
import {getFocusBlock} from '../selectors/selector.get-focus-block'
55
import {getFocusInlineObject} from '../selectors/selector.get-focus-inline-object'
6+
import {getFocusTextBlock} from '../selectors/selector.get-focus-text-block'
7+
import {getNextBlock} from '../selectors/selector.get-next-block'
68
import {getPreviousBlock} from '../selectors/selector.get-previous-block'
79
import {isSelectionCollapsed} from '../selectors/selector.is-selection-collapsed'
810
import {isSelectionExpanded} from '../selectors/selector.is-selection-expanded'
11+
import {isEqualSelectionPoints} from '../utils'
912
import {getBlockEndPoint} from '../utils/util.get-block-end-point'
13+
import {getBlockStartPoint} from '../utils/util.get-block-start-point'
1014
import {isEmptyTextBlock} from '../utils/util.is-empty-text-block'
1115
import {raise} from './behavior.types.action'
1216
import {defineBehavior} from './behavior.types.behavior'
1317

14-
const shiftLeft = createKeyboardShortcut({
15-
default: [
16-
{
17-
key: 'ArrowLeft',
18-
shift: true,
19-
meta: false,
20-
ctrl: false,
21-
alt: false,
22-
},
23-
],
24-
})
25-
2618
export const abstractKeyboardBehaviors = [
2719
/**
2820
* When Backspace is pressed on an inline object, Slate will raise a
@@ -130,7 +122,10 @@ export const abstractKeyboardBehaviors = [
130122
defineBehavior({
131123
on: 'keyboard.keydown',
132124
guard: ({snapshot, event}) => {
133-
if (!snapshot.context.selection || !shiftLeft.guard(event.originEvent)) {
125+
if (
126+
!snapshot.context.selection ||
127+
!defaultKeyboardShortcuts.shiftLeft.guard(event.originEvent)
128+
) {
134129
return false
135130
}
136131

@@ -186,4 +181,260 @@ export const abstractKeyboardBehaviors = [
186181
],
187182
],
188183
}),
184+
185+
defineBehavior({
186+
on: 'keyboard.keydown',
187+
guard: ({snapshot, event}) => {
188+
if (
189+
!snapshot.context.selection ||
190+
!defaultKeyboardShortcuts.shiftDown.guard(event.originEvent)
191+
) {
192+
return false
193+
}
194+
195+
const focusBlockObject = getFocusBlockObject(snapshot)
196+
197+
if (!focusBlockObject) {
198+
return false
199+
}
200+
201+
const nextBlock = getNextBlock(snapshot)
202+
203+
if (!nextBlock) {
204+
return false
205+
}
206+
207+
if (!isTextBlock(snapshot.context, nextBlock.node)) {
208+
return {
209+
nextBlockEndPoint: getBlockEndPoint({
210+
context: snapshot.context,
211+
block: nextBlock,
212+
}),
213+
selection: snapshot.context.selection,
214+
}
215+
}
216+
217+
const nextNextBlock = getNextBlock({
218+
...snapshot,
219+
context: {
220+
...snapshot.context,
221+
selection: {
222+
anchor: {
223+
path: nextBlock.path,
224+
offset: 0,
225+
},
226+
focus: {
227+
path: nextBlock.path,
228+
offset: 0,
229+
},
230+
},
231+
},
232+
})
233+
234+
const nextBlockEndPoint =
235+
nextNextBlock && isTextBlock(snapshot.context, nextNextBlock.node)
236+
? getBlockStartPoint({
237+
context: snapshot.context,
238+
block: nextNextBlock,
239+
})
240+
: getBlockEndPoint({
241+
context: snapshot.context,
242+
block: nextBlock,
243+
})
244+
245+
return {nextBlockEndPoint, selection: snapshot.context.selection}
246+
},
247+
actions: [
248+
(_, {nextBlockEndPoint, selection}) => [
249+
raise({
250+
type: 'select',
251+
at: {anchor: selection.anchor, focus: nextBlockEndPoint},
252+
}),
253+
],
254+
],
255+
}),
256+
257+
defineBehavior({
258+
on: 'keyboard.keydown',
259+
guard: ({snapshot, event, dom}) => {
260+
if (
261+
!snapshot.context.selection ||
262+
!defaultKeyboardShortcuts.shiftDown.guard(event.originEvent)
263+
) {
264+
return false
265+
}
266+
267+
const focusTextBlock = getFocusTextBlock(snapshot)
268+
269+
if (!focusTextBlock) {
270+
return false
271+
}
272+
273+
const nextBlock = getNextBlock(snapshot)
274+
275+
if (!nextBlock) {
276+
return false
277+
}
278+
279+
if (isTextBlock(snapshot.context, nextBlock.node)) {
280+
return false
281+
}
282+
283+
const focusBlockEndPoint = getBlockEndPoint({
284+
context: snapshot.context,
285+
block: focusTextBlock,
286+
})
287+
288+
if (
289+
isEqualSelectionPoints(
290+
snapshot.context.selection.focus,
291+
focusBlockEndPoint,
292+
)
293+
) {
294+
return false
295+
}
296+
297+
// Find the DOM position of the current focus point
298+
const focusRect = dom.getSelectionRect({
299+
...snapshot,
300+
context: {
301+
...snapshot.context,
302+
selection: {
303+
anchor: snapshot.context.selection.focus,
304+
focus: snapshot.context.selection.focus,
305+
},
306+
},
307+
})
308+
// Find the DOM position of the focus block end point
309+
const endPointRect = dom.getSelectionRect({
310+
...snapshot,
311+
context: {
312+
...snapshot.context,
313+
selection: {
314+
anchor: focusBlockEndPoint,
315+
focus: focusBlockEndPoint,
316+
},
317+
},
318+
})
319+
320+
if (!focusRect || !endPointRect) {
321+
return false
322+
}
323+
324+
if (endPointRect.top > focusRect.top) {
325+
// If the end point is positioned further from the top than the current
326+
// focus point, then we can deduce that the end point is on the next
327+
// line. In this case, we don't want to interfere since the browser
328+
// does right thing and expands the selection to the end of the current
329+
// line.
330+
return false
331+
}
332+
333+
// If the end point is positioned at the same level as the current focus
334+
// point, then we can deduce that the end point is on the same line. In
335+
// this case, we want to expand the selection to the end point.
336+
// This mitigates a Firefox bug where Shift+ArrowDown can expand
337+
// further into the next block.
338+
return {focusBlockEndPoint, selection: snapshot.context.selection}
339+
},
340+
actions: [
341+
(_, {focusBlockEndPoint, selection}) => [
342+
raise({
343+
type: 'select',
344+
at: {
345+
anchor: selection.anchor,
346+
focus: focusBlockEndPoint,
347+
},
348+
}),
349+
],
350+
],
351+
}),
352+
353+
defineBehavior({
354+
on: 'keyboard.keydown',
355+
guard: ({snapshot, event, dom}) => {
356+
if (
357+
!snapshot.context.selection ||
358+
!defaultKeyboardShortcuts.shiftDown.guard(event.originEvent)
359+
) {
360+
return false
361+
}
362+
363+
const focusTextBlock = getFocusTextBlock(snapshot)
364+
365+
if (!focusTextBlock) {
366+
return false
367+
}
368+
369+
const nextBlock = getNextBlock(snapshot)
370+
371+
if (!nextBlock) {
372+
return false
373+
}
374+
375+
const focusBlockEndPoint = getBlockEndPoint({
376+
context: snapshot.context,
377+
block: focusTextBlock,
378+
})
379+
380+
// Find the DOM position of the current focus point
381+
const focusRect = dom.getSelectionRect({
382+
...snapshot,
383+
context: {
384+
...snapshot.context,
385+
selection: {
386+
anchor: snapshot.context.selection.focus,
387+
focus: snapshot.context.selection.focus,
388+
},
389+
},
390+
})
391+
// Find the DOM position of the focus block end point
392+
const endPointRect = dom.getSelectionRect({
393+
...snapshot,
394+
context: {
395+
...snapshot.context,
396+
selection: {
397+
anchor: focusBlockEndPoint,
398+
focus: focusBlockEndPoint,
399+
},
400+
},
401+
})
402+
403+
if (!focusRect || !endPointRect) {
404+
return false
405+
}
406+
407+
if (endPointRect.top > focusRect.top) {
408+
// If the end point is positioned further from the top than the current
409+
// focus point, then we can deduce that the end point is on the next
410+
// line. In this case, we don't want to interfere since the browser
411+
// does right thing and expands the selection to the end of the current
412+
// line.
413+
return false
414+
}
415+
416+
// If the end point is positioned at the same level as the current focus
417+
// point, then we can deduce that the end point is on the same line. In
418+
// this case, we want to expand the selection to the end of the start
419+
// block. This mitigates a Chromium bug where Shift+ArrowDown can expand
420+
// further into the next block.
421+
const nextBlockStartPoint = getBlockStartPoint({
422+
context: snapshot.context,
423+
block: nextBlock,
424+
})
425+
426+
return {nextBlockStartPoint, selection: snapshot.context.selection}
427+
},
428+
actions: [
429+
(_, {nextBlockStartPoint, selection}) => [
430+
raise({
431+
type: 'select',
432+
at: {
433+
anchor: selection.anchor,
434+
focus: nextBlockStartPoint,
435+
},
436+
}),
437+
],
438+
],
439+
}),
189440
]

packages/editor/src/keyboard-shortcuts/default-keyboard-shortcuts.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,28 @@ export const defaultKeyboardShortcuts = {
132132
},
133133
],
134134
}),
135+
shiftDown: createKeyboardShortcut({
136+
default: [
137+
{
138+
key: 'ArrowDown',
139+
shift: true,
140+
meta: false,
141+
ctrl: false,
142+
alt: false,
143+
},
144+
],
145+
}),
146+
shiftLeft: createKeyboardShortcut({
147+
default: [
148+
{
149+
key: 'ArrowLeft',
150+
shift: true,
151+
meta: false,
152+
ctrl: false,
153+
alt: false,
154+
},
155+
],
156+
}),
135157
shiftTab: createKeyboardShortcut({
136158
default: [
137159
{

0 commit comments

Comments
 (0)