From 92ec4c8035e6f0b68127327f2525f210ecf9efd3 Mon Sep 17 00:00:00 2001 From: Xwatson Date: Fri, 28 Aug 2026 14:43:27 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20#WIK-20030=20=E9=A1=B5=E9=9D=A2?= =?UTF-8?q?=E4=B8=AD=E6=B7=BB=E5=8A=A0=E2=80=9C=E6=A0=87=E7=AD=BE=E2=80=9D?= =?UTF-8?q?=EF=BC=8C=E7=81=AB=E7=8B=90=E6=B5=8F=E8=A7=88=E5=99=A8=E4=B8=AD?= =?UTF-8?q?=E5=A4=B1=E7=84=A6=E5=AF=BC=E8=87=B4=E6=A0=87=E7=AD=BE=E9=97=AA?= =?UTF-8?q?=E9=80=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../editable/editable.component.spec.ts | 67 ++++++++++++++++++- .../components/editable/editable.component.ts | 15 ++++- 2 files changed, 79 insertions(+), 3 deletions(-) diff --git a/packages/src/components/editable/editable.component.spec.ts b/packages/src/components/editable/editable.component.spec.ts index 99f19504..cde68a41 100644 --- a/packages/src/components/editable/editable.component.spec.ts +++ b/packages/src/components/editable/editable.component.spec.ts @@ -1,8 +1,8 @@ import { ComponentFixture, fakeAsync, flush, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { AngularEditor } from 'slate-angular'; -import { AdvancedEditableComponent, configureBasicEditableTestingModule, dispatchFakeEvent } from '../../testing'; -import { Editor, Transforms } from 'slate'; +import { AdvancedEditableComponent, configureBasicEditableTestingModule, dispatchFakeEvent, dispatchMouseEvent } from '../../testing'; +import { Editor, Node, Range, Transforms } from 'slate'; describe('Editable Component', () => { let component: AdvancedEditableComponent; @@ -181,4 +181,67 @@ describe('Editable Component', () => { expect(component.scrollSelectionIntoView).toHaveBeenCalledTimes(1); })); + + it('should not steal focus from an external control while native selection sync is pending', fakeAsync(() => { + fixture.detectChanges(); + flush(); + fixture.detectChanges(); + + const editor = component.editor; + const editorElement = AngularEditor.toDOMNode(editor, editor) as HTMLElement; + const externalInput = document.createElement('input'); + document.body.appendChild(externalInput); + + try { + editorElement.focus(); + Transforms.select(editor, Editor.start(editor, [0])); + flush(); + + const end = Editor.end(editor, [0]); + editor.selection = { anchor: end, focus: end }; + component.editableComponent.toNativeSelection(false); + + externalInput.focus(); + flush(); + + expect(document.activeElement).toBe(externalInput); + } finally { + externalInput.remove(); + } + })); + + it('should resync Slate selection when focus returns and the DOM range is unchanged', fakeAsync(() => { + fixture.detectChanges(); + flush(); + fixture.detectChanges(); + + const editor = component.editor; + const editorElement = AngularEditor.toDOMNode(editor, editor) as HTMLElement; + const textElement = AngularEditor.toDOMNode(editor, Node.get(editor, [0, 0])); + const domRange: Range = { + anchor: { path: [0, 0], offset: 4 }, + focus: { path: [0, 0], offset: 4 } + }; + const staleSlateRange: Range = { + anchor: { path: [0, 0], offset: 12 }, + focus: { path: [0, 0], offset: 12 } + }; + + editorElement.focus(); + const nativeRange = AngularEditor.toDOMRange(editor, domRange); + document + .getSelection()! + .setBaseAndExtent(nativeRange.startContainer, nativeRange.startOffset, nativeRange.endContainer, nativeRange.endOffset); + + // Simulate an iframe relay updating Slate while the native selection stays behind. + spyOn(component.editableComponent, 'toNativeSelection').and.stub(); + Transforms.select(editor, staleSlateRange); + expect(editor.selection).toEqual(staleSlateRange); + + dispatchMouseEvent(textElement, 'click'); + + expect(document.activeElement).toBe(editorElement); + expect(editor.selection).toEqual(domRange); + editorElement.blur(); + })); }); diff --git a/packages/src/components/editable/editable.component.ts b/packages/src/components/editable/editable.component.ts index d1c067c9..e7dc51ee 100644 --- a/packages/src/components/editable/editable.component.ts +++ b/packages/src/components/editable/editable.component.ts @@ -1039,7 +1039,12 @@ export class SlateEditable implements OnInit, OnChanges, OnDestroy, AfterViewChe newDomRange && autoScroll && this.scrollSelectionIntoView(this.editor, newDomRange); // COMPAT: In Firefox, it's not enough to create a range, you also need // to focus the contenteditable element too. (2016/11/16) - if (newDomRange && IS_FIREFOX) { + // Don't steal focus if another control was focused while this callback was queued. + const currentActiveElement = root.activeElement; + const documentBody = (root as Document).body; + const hasAnotherFocusedElement = + !!currentActiveElement && currentActiveElement !== el && currentActiveElement !== documentBody; + if (newDomRange && IS_FIREFOX && !hasAnotherFocusedElement) { el.focus(); } } @@ -1543,6 +1548,14 @@ export class SlateEditable implements OnInit, OnChanges, OnDestroy, AfterViewChe !this.isDOMEventHandled(event, this.click) && isDOMNode(event.target) ) { + // Browsers do not fire selectionchange when clicking the same DOM position. + // Sync it explicitly after focus returns from an embedded element. + const root = AngularEditor.findDocumentOrShadowRoot(this.editor); + const editorElement = AngularEditor.toDOMNode(this.editor, this.editor); + if (root.activeElement === editorElement) { + this.toSlateSelection(); + } + const node = AngularEditor.toSlateNode(this.editor, event.target); const path = AngularEditor.findPath(this.editor, node); const start = Editor.start(this.editor, path); From e3e3228ecde9acfae6a98226828757d01617f1c9 Mon Sep 17 00:00:00 2001 From: Xwatson Date: Fri, 28 Aug 2026 14:58:57 +0800 Subject: [PATCH 2/2] fix: optimize --- .../editable/editable.component.spec.ts | 39 +------------------ .../components/editable/editable.component.ts | 8 ---- 2 files changed, 2 insertions(+), 45 deletions(-) diff --git a/packages/src/components/editable/editable.component.spec.ts b/packages/src/components/editable/editable.component.spec.ts index cde68a41..5cd9bae1 100644 --- a/packages/src/components/editable/editable.component.spec.ts +++ b/packages/src/components/editable/editable.component.spec.ts @@ -1,8 +1,8 @@ import { ComponentFixture, fakeAsync, flush, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { AngularEditor } from 'slate-angular'; -import { AdvancedEditableComponent, configureBasicEditableTestingModule, dispatchFakeEvent, dispatchMouseEvent } from '../../testing'; -import { Editor, Node, Range, Transforms } from 'slate'; +import { AdvancedEditableComponent, configureBasicEditableTestingModule, dispatchFakeEvent } from '../../testing'; +import { Editor, Transforms } from 'slate'; describe('Editable Component', () => { let component: AdvancedEditableComponent; @@ -209,39 +209,4 @@ describe('Editable Component', () => { externalInput.remove(); } })); - - it('should resync Slate selection when focus returns and the DOM range is unchanged', fakeAsync(() => { - fixture.detectChanges(); - flush(); - fixture.detectChanges(); - - const editor = component.editor; - const editorElement = AngularEditor.toDOMNode(editor, editor) as HTMLElement; - const textElement = AngularEditor.toDOMNode(editor, Node.get(editor, [0, 0])); - const domRange: Range = { - anchor: { path: [0, 0], offset: 4 }, - focus: { path: [0, 0], offset: 4 } - }; - const staleSlateRange: Range = { - anchor: { path: [0, 0], offset: 12 }, - focus: { path: [0, 0], offset: 12 } - }; - - editorElement.focus(); - const nativeRange = AngularEditor.toDOMRange(editor, domRange); - document - .getSelection()! - .setBaseAndExtent(nativeRange.startContainer, nativeRange.startOffset, nativeRange.endContainer, nativeRange.endOffset); - - // Simulate an iframe relay updating Slate while the native selection stays behind. - spyOn(component.editableComponent, 'toNativeSelection').and.stub(); - Transforms.select(editor, staleSlateRange); - expect(editor.selection).toEqual(staleSlateRange); - - dispatchMouseEvent(textElement, 'click'); - - expect(document.activeElement).toBe(editorElement); - expect(editor.selection).toEqual(domRange); - editorElement.blur(); - })); }); diff --git a/packages/src/components/editable/editable.component.ts b/packages/src/components/editable/editable.component.ts index e7dc51ee..54b95752 100644 --- a/packages/src/components/editable/editable.component.ts +++ b/packages/src/components/editable/editable.component.ts @@ -1548,14 +1548,6 @@ export class SlateEditable implements OnInit, OnChanges, OnDestroy, AfterViewChe !this.isDOMEventHandled(event, this.click) && isDOMNode(event.target) ) { - // Browsers do not fire selectionchange when clicking the same DOM position. - // Sync it explicitly after focus returns from an embedded element. - const root = AngularEditor.findDocumentOrShadowRoot(this.editor); - const editorElement = AngularEditor.toDOMNode(this.editor, this.editor); - if (root.activeElement === editorElement) { - this.toSlateSelection(); - } - const node = AngularEditor.toSlateNode(this.editor, event.target); const path = AngularEditor.findPath(this.editor, node); const start = Editor.start(this.editor, path);