Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a Time-Based Debouncer to update Screen Readers at most once per second #3399

Merged
merged 11 commits into from
Aug 18, 2021
Merged
8 changes: 4 additions & 4 deletions src/browser/AccessibilityManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
*/

import * as Strings from 'browser/LocalizableStrings';
import { ITerminal } from 'browser/Types';
import { ITerminal, IRenderDebouncer } from 'browser/Types';
import { IBuffer } from 'common/buffer/Types';
import { isMac } from 'common/Platform';
import { RenderDebouncer } from 'browser/RenderDebouncer';
import { TimeBasedDebouncer } from 'browser/TimeBasedDebouncer';
import { addDisposableDomListener } from 'browser/Lifecycle';
import { Disposable } from 'common/Lifecycle';
import { ScreenDprMonitor } from 'browser/ScreenDprMonitor';
Expand All @@ -28,7 +28,7 @@ export class AccessibilityManager extends Disposable {
private _liveRegion: HTMLElement;
private _liveRegionLineCount: number = 0;

private _renderRowsDebouncer: RenderDebouncer;
private _renderRowsDebouncer: IRenderDebouncer;
private _screenDprMonitor: ScreenDprMonitor;

private _topBoundaryFocusListener: (e: FocusEvent) => void;
Expand Down Expand Up @@ -72,7 +72,7 @@ export class AccessibilityManager extends Disposable {
this._refreshRowsDimensions();
this._accessibilityTreeRoot.appendChild(this._rowContainer);

this._renderRowsDebouncer = new RenderDebouncer(this._renderRows.bind(this));
this._renderRowsDebouncer = new TimeBasedDebouncer(this._renderRows.bind(this));
this._refreshRows();

this._liveRegion = document.createElement('div');
Expand Down
4 changes: 2 additions & 2 deletions src/browser/RenderDebouncer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
* @license MIT
*/

import { IDisposable } from 'common/Types';
import { IRenderDebouncer } from 'browser/Types';

/**
* Debounces calls to render terminal rows using animation frames.
*/
export class RenderDebouncer implements IDisposable {
export class RenderDebouncer implements IRenderDebouncer {
private _rowStart: number | undefined;
private _rowEnd: number | undefined;
private _rowCount: number | undefined;
Expand Down
86 changes: 86 additions & 0 deletions src/browser/TimeBasedDebouncer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/

const RENDER_DEBOUNCE_THRESHOLD_MS = 1000; // 1 Second
pattch marked this conversation as resolved.
Show resolved Hide resolved

import { IRenderDebouncer } from 'browser/Types';

/**
* Debounces calls to update screen readers to update at most once configurable interval of time.
*/
export class TimeBasedDebouncer implements IRenderDebouncer {
private _rowStart: number | undefined;
private _rowEnd: number | undefined;
private _rowCount: number | undefined;

// The last moment that the Terminal was refreshed at
private _lastRefreshMs = 0;
// Whether a trailing refresh should be triggered due to a refresh request that was throttled
private _additionalRefreshRequested = false;

private _refreshTimeoutID: number | undefined;

constructor(
private _renderCallback: (start: number, end: number) => void,
private readonly _debounceThresholdMS = RENDER_DEBOUNCE_THRESHOLD_MS
) {
}

public dispose(): void {
if (this._refreshTimeoutID) {
clearTimeout(this._refreshTimeoutID);
}
}

public refresh(rowStart: number | undefined, rowEnd: number | undefined, rowCount: number): void {
this._rowCount = rowCount;
// Get the min/max row start/end for the arg values
rowStart = rowStart !== undefined ? rowStart : 0;
rowEnd = rowEnd !== undefined ? rowEnd : this._rowCount - 1;
// Set the properties to the updated values
this._rowStart = this._rowStart !== undefined ? Math.min(this._rowStart, rowStart) : rowStart;
this._rowEnd = this._rowEnd !== undefined ? Math.max(this._rowEnd, rowEnd) : rowEnd;

// Only refresh if the time since last refresh is above a threshold, otherwise wait for
// enough time to pass before refreshing again.
const refreshRequestTime: number = Date.now();
if (refreshRequestTime - this._lastRefreshMs >= this._debounceThresholdMS) {
// Enough time has lapsed since the last refresh; refresh immediately
this._lastRefreshMs = refreshRequestTime;
this._innerRefresh();
} else if (!this._additionalRefreshRequested) {
// This is the first additional request throttled; set up trailing refresh
const elapsed = refreshRequestTime - this._lastRefreshMs;
const waitPeriodBeforeTrailingRefresh = this._debounceThresholdMS - elapsed;
this._additionalRefreshRequested = true;

this._refreshTimeoutID = window.setTimeout(() => {
this._lastRefreshMs = Date.now();
this._innerRefresh();
this._additionalRefreshRequested = false;
this._refreshTimeoutID = undefined; // No longer need to clear the timeout
}, waitPeriodBeforeTrailingRefresh);
}
}

private _innerRefresh(): void {
// Make sure values are set
if (this._rowStart === undefined || this._rowEnd === undefined || this._rowCount === undefined) {
return;
}

// Clamp values
const start = Math.max(this._rowStart, 0);
const end = Math.min(this._rowEnd, this._rowCount - 1);

// Reset debouncer (this happens before render callback as the render could trigger it again)
this._rowStart = undefined;
this._rowEnd = undefined;

// Run render callback
this._renderCallback(start, end);
}
}

4 changes: 4 additions & 0 deletions src/browser/Types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -309,3 +309,7 @@ export interface ICharacterJoiner {
id: number;
handler: CharacterJoinerHandler;
}

export interface IRenderDebouncer extends IDisposable {
refresh(rowStart: number | undefined, rowEnd: number | undefined, rowCount: number): void;
}
4 changes: 2 additions & 2 deletions src/browser/services/RenderService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { EventEmitter, IEvent } from 'common/EventEmitter';
import { Disposable } from 'common/Lifecycle';
import { ScreenDprMonitor } from 'browser/ScreenDprMonitor';
import { addDisposableDomListener } from 'browser/Lifecycle';
import { IColorSet } from 'browser/Types';
import { IColorSet, IRenderDebouncer } from 'browser/Types';
import { IOptionsService, IBufferService } from 'common/services/Services';
import { ICharSizeService, IRenderService } from 'browser/services/Services';

Expand All @@ -22,7 +22,7 @@ interface ISelectionState {
export class RenderService extends Disposable implements IRenderService {
public serviceBrand: undefined;

private _renderDebouncer: RenderDebouncer;
private _renderDebouncer: IRenderDebouncer;
private _screenDprMonitor: ScreenDprMonitor;

private _isPaused: boolean = false;
Expand Down