Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/browser/controller/editContext/native/screenReaderContentSimple.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { addDisposableListener, getActiveWindow } from '../../../../../base/browser/dom.js';
7
import { FastDomNode } from '../../../../../base/browser/fastDomNode.js';
8
import { AccessibilitySupport, IAccessibilityService } from '../../../../../platform/accessibility/common/accessibility.js';
9
import { EditorOption, IComputedEditorOptions } from '../../../../common/config/editorOptions.js';
10
import { EndOfLineSequence } from '../../../../common/model.js';
11
import { ViewContext } from '../../../../common/viewModel/viewContext.js';
12
import { Selection } from '../../../../common/core/selection.js';
13
import { SimplePagedScreenReaderStrategy, ISimpleScreenReaderContentState } from '../screenReaderUtils.js';
14
import { PositionOffsetTransformer } from '../../../../common/core/text/positionToOffset.js';
15
import { Disposable, IDisposable, MutableDisposable } from '../../../../../base/common/lifecycle.js';
16
import { IME } from '../../../../../base/common/ime.js';
17
import { ViewController } from '../../../view/viewController.js';
18
import { IScreenReaderContent } from './screenReaderUtils.js';
19
20
export class SimpleScreenReaderContent extends Disposable implements IScreenReaderContent {
21
22
private readonly _selectionChangeListener = this._register(new MutableDisposable());
23
24
private _accessibilityPageSize: number = 1;
25
private _ignoreSelectionChangeTime: number = 0;
26
27
private _state: ISimpleScreenReaderContentState | undefined;
28
private _strategy: SimplePagedScreenReaderStrategy = new SimplePagedScreenReaderStrategy();
29
30
constructor(
31
private readonly _domNode: FastDomNode<HTMLElement>,
32
private readonly _context: ViewContext,
33
private readonly _viewController: ViewController,
34
@IAccessibilityService private readonly _accessibilityService: IAccessibilityService
35
) {
36
super();
37
this.onConfigurationChanged(this._context.configuration.options);
38
}
39
40
public updateScreenReaderContent(primarySelection: Selection): void {
41
const domNode = this._domNode.domNode;
42
const focusedElement = getActiveWindow().document.activeElement;
43
if (!focusedElement || focusedElement !== domNode) {
44
return;
45
}
46
const isScreenReaderOptimized = this._accessibilityService.isScreenReaderOptimized();
47
if (isScreenReaderOptimized) {
48
this._state = this._getScreenReaderContentState(primarySelection);
49
if (domNode.textContent !== this._state.value) {
50
this._setIgnoreSelectionChangeTime('setValue');
51
domNode.textContent = this._state.value;
52
}
53
const selection = getActiveWindow().document.getSelection();
54
if (!selection) {
55
return;
56
}
57
const data = this._getScreenReaderRange(this._state.selectionStart, this._state.selectionEnd);
58
if (!data) {
59
return;
60
}
61
this._setIgnoreSelectionChangeTime('setRange');
62
selection.setBaseAndExtent(
63
data.anchorNode,
64
data.anchorOffset,
65
data.focusNode,
66
data.focusOffset
67
);
68
} else {
69
this._state = undefined;
70
this._setIgnoreSelectionChangeTime('setValue');
71
this._domNode.domNode.textContent = '';
72
}
73
}
74
75
public updateScrollTop(primarySelection: Selection): void {
76
if (!this._state) {
77
return;
78
}
79
const viewLayout = this._context.viewModel.viewLayout;
80
const stateStartLineNumber = this._state.startPositionWithinEditor.lineNumber;
81
const verticalOffsetOfStateStartLineNumber = viewLayout.getVerticalOffsetForLineNumber(stateStartLineNumber);
82
const verticalOffsetOfPositionLineNumber = viewLayout.getVerticalOffsetForLineNumber(primarySelection.positionLineNumber);
83
this._domNode.domNode.scrollTop = verticalOffsetOfPositionLineNumber - verticalOffsetOfStateStartLineNumber;
84
}
85
86
public onFocusChange(newFocusValue: boolean): void {
87
if (newFocusValue) {
88
this._selectionChangeListener.value = this._setSelectionChangeListener();
89
} else {
90
this._selectionChangeListener.value = undefined;
91
}
92
}
93
94
public onConfigurationChanged(options: IComputedEditorOptions): void {
95
this._accessibilityPageSize = options.get(EditorOption.accessibilityPageSize);
96
}
97
98
public onWillCut(): void {
99
this._setIgnoreSelectionChangeTime('onCut');
100
}
101
102
public onWillPaste(): void {
103
this._setIgnoreSelectionChangeTime('onWillPaste');
104
}
105
106
// --- private methods
107
108
public _setIgnoreSelectionChangeTime(reason: string): void {
109
this._ignoreSelectionChangeTime = Date.now();
110
}
111
112
private _setSelectionChangeListener(): IDisposable {
113
// See https://github.com/microsoft/vscode/issues/27216 and https://github.com/microsoft/vscode/issues/98256
114
// When using a Braille display or NVDA for example, it is possible for users to reposition the
115
// system caret. This is reflected in Chrome as a `selectionchange` event and needs to be reflected within the editor.
116
117
// `selectionchange` events often come multiple times for a single logical change
118
// so throttle multiple `selectionchange` events that burst in a short period of time.
119
let previousSelectionChangeEventTime = 0;
120
return addDisposableListener(this._domNode.domNode.ownerDocument, 'selectionchange', () => {
121
const isScreenReaderOptimized = this._accessibilityService.isScreenReaderOptimized();
122
if (!this._state || !isScreenReaderOptimized || !IME.enabled) {
123
return;
124
}
125
const activeElement = getActiveWindow().document.activeElement;
126
const isFocused = activeElement === this._domNode.domNode;
127
if (!isFocused) {
128
return;
129
}
130
const selection = getActiveWindow().document.getSelection();
131
if (!selection) {
132
return;
133
}
134
const rangeCount = selection.rangeCount;
135
if (rangeCount === 0) {
136
return;
137
}
138
const range = selection.getRangeAt(0);
139
140
const now = Date.now();
141
const delta1 = now - previousSelectionChangeEventTime;
142
previousSelectionChangeEventTime = now;
143
if (delta1 < 5) {
144
// received another `selectionchange` event within 5ms of the previous `selectionchange` event
145
// => ignore it
146
return;
147
}
148
const delta2 = now - this._ignoreSelectionChangeTime;
149
this._ignoreSelectionChangeTime = 0;
150
if (delta2 < 100) {
151
// received a `selectionchange` event within 100ms since we touched the hidden div
152
// => ignore it, since we caused it
153
return;
154
}
155
156
this._viewController.setSelection(this._getEditorSelectionFromDomRange(this._context, this._state, selection.direction, range));
157
});
158
}
159
160
private _getScreenReaderContentState(primarySelection: Selection): ISimpleScreenReaderContentState {
161
const state = this._strategy.fromEditorSelection(
162
this._context.viewModel,
163
primarySelection,
164
this._accessibilityPageSize,
165
this._accessibilityService.getAccessibilitySupport() === AccessibilitySupport.Unknown
166
);
167
const endPosition = this._context.viewModel.model.getPositionAt(Infinity);
168
let value = state.value;
169
if (endPosition.column === 1 && primarySelection.getEndPosition().equals(endPosition)) {
170
value += '\n';
171
}
172
state.value = value;
173
return state;
174
}
175
176
private _getScreenReaderRange(selectionOffsetStart: number, selectionOffsetEnd: number): { anchorNode: Node; anchorOffset: number; focusNode: Node; focusOffset: number } | undefined {
177
const textContent = this._domNode.domNode.firstChild;
178
if (!textContent) {
179
return;
180
}
181
const range = new globalThis.Range();
182
range.setStart(textContent, selectionOffsetStart);
183
range.setEnd(textContent, selectionOffsetEnd);
184
return {
185
anchorNode: textContent,
186
anchorOffset: selectionOffsetStart,
187
focusNode: textContent,
188
focusOffset: selectionOffsetEnd
189
};
190
}
191
192
private _getEditorSelectionFromDomRange(context: ViewContext, state: ISimpleScreenReaderContentState, direction: string, range: globalThis.Range): Selection {
193
const viewModel = context.viewModel;
194
const model = viewModel.model;
195
const coordinatesConverter = viewModel.coordinatesConverter;
196
const modelScreenReaderContentStartPositionWithinEditor = coordinatesConverter.convertViewPositionToModelPosition(state.startPositionWithinEditor);
197
const offsetOfStartOfScreenReaderContent = model.getOffsetAt(modelScreenReaderContentStartPositionWithinEditor);
198
let offsetOfSelectionStart = range.startOffset + offsetOfStartOfScreenReaderContent;
199
let offsetOfSelectionEnd = range.endOffset + offsetOfStartOfScreenReaderContent;
200
const modelUsesCRLF = model.getEndOfLineSequence() === EndOfLineSequence.CRLF;
201
if (modelUsesCRLF) {
202
const screenReaderContentText = state.value;
203
const offsetTransformer = new PositionOffsetTransformer(screenReaderContentText);
204
const positionOfStartWithinText = offsetTransformer.getPosition(range.startOffset);
205
const positionOfEndWithinText = offsetTransformer.getPosition(range.endOffset);
206
offsetOfSelectionStart += positionOfStartWithinText.lineNumber - 1;
207
offsetOfSelectionEnd += positionOfEndWithinText.lineNumber - 1;
208
}
209
const positionOfSelectionStart = model.getPositionAt(offsetOfSelectionStart);
210
const positionOfSelectionEnd = model.getPositionAt(offsetOfSelectionEnd);
211
const selectionStart = direction === 'forward' ? positionOfSelectionStart : positionOfSelectionEnd;
212
const selectionEnd = direction === 'forward' ? positionOfSelectionEnd : positionOfSelectionStart;
213
return Selection.fromPositions(selectionStart, selectionEnd);
214
}
215
}
216
217