Skip to content

Commit

Permalink
Merge pull request #3399 from pattch/master
Browse files Browse the repository at this point in the history
Add a Time-Based Debouncer to update Screen Readers at most once per second
  • Loading branch information
Tyriar authored Aug 18, 2021
2 parents 9cdb806 + 42e0fcd commit 9490c5c
Show file tree
Hide file tree
Showing 5 changed files with 98 additions and 8 deletions.
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

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

0 comments on commit 9490c5c

Please sign in to comment.