dockview-core
Version:
Zero dependency layout manager supporting tabs, groups, grids and splitviews for vanilla TypeScript
186 lines (185 loc) • 8.28 kB
JavaScript
;
var __extends = (this && this.__extends) || (function () {
var extendStatics = function (d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
return extendStatics(d, b);
};
return function (d, b) {
if (typeof b !== "function" && b !== null)
throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.LiveRegionModule = exports.LiveRegionService = void 0;
var lifecycle_1 = require("../lifecycle");
var accessibilityMessages_1 = require("./accessibilityMessages");
var modules_1 = require("./modules");
/** Bulk transactions whose per-panel events should not each be announced. */
var isBulk = function (kind) {
return kind === 'load' || kind === 'clear';
};
function createLiveRegion(politeness) {
var el = document.createElement('div');
el.className =
politeness === 'assertive'
? 'dv-live-region-assertive'
: 'dv-live-region';
// assertive interrupts the SR (errors / cancellations); polite waits for a
// pause (routine status). `alert` implies assertive, `status` implies polite.
el.setAttribute('role', politeness === 'assertive' ? 'alert' : 'status');
el.setAttribute('aria-live', politeness);
el.setAttribute('aria-atomic', 'true');
// Visually hidden but kept in the accessibility tree (never display:none /
// visibility:hidden, which would drop it from AT). Standard clip pattern.
Object.assign(el.style, {
position: 'absolute',
width: '1px',
height: '1px',
margin: '-1px',
padding: '0',
overflow: 'hidden',
clip: 'rect(0 0 0 0)',
clipPath: 'inset(50%)',
whiteSpace: 'nowrap',
border: '0',
});
return el;
}
/**
* Narrates layout state changes to screen readers via visually-hidden
* `aria-live` regions. Free / core (WCAG 4.1.3). Announces panel open/close +
* the shared `announce()` sink (the accessibility module narrates docking here too).
* Two regions: a **polite** one for routine status and an **assertive** one
* for errors/cancellations. The bulk load/clear burst is suppressed via the
* mutation-transaction events, and an app can take over delivery entirely with
* the `announcer` option.
*/
var LiveRegionService = /** @class */ (function (_super) {
__extends(LiveRegionService, _super);
function LiveRegionService(host) {
var _this = _super.call(this) || this;
_this._suppressDepth = 0;
_this._locationSubs = new Map();
_this._host = host;
_this._polite = createLiveRegion('polite');
_this._assertive = createLiveRegion('assertive');
host.element.appendChild(_this._polite);
host.element.appendChild(_this._assertive);
_this.addDisposables({ dispose: function () { return _this._polite.remove(); } }, { dispose: function () { return _this._assertive.remove(); } }, host.onDidAddPanel(function (panel) { return _this._announce(panel, 'open'); }), host.onDidRemovePanel(function (panel) { return _this._announce(panel, 'close'); }),
// Bracket bulk transactions so a fromJSON / clear doesn't announce
// every nested add/remove.
host.onWillMutateLayout(function (e) {
if (isBulk(e.kind)) {
_this._suppressDepth++;
}
}), host.onDidMutateLayout(function (e) {
if (isBulk(e.kind)) {
_this._suppressDepth = Math.max(0, _this._suppressDepth - 1);
}
}), host.onDidMaximizedGroupChange(function (e) {
var panel = e.group.activePanel;
if (panel) {
_this._announce(panel, e.isMaximized ? 'maximize' : 'restore');
}
}),
// Narrate a group floating / docking back / popping out. A group is
// born in the grid then transitions, so track each group's previous
// location and ignore the no-op initial `-> grid`.
host.onDidAddGroup(function (group) { return _this._trackLocation(group); }), host.onDidRemoveGroup(function (group) {
var _a;
(_a = _this._locationSubs.get(group.id)) === null || _a === void 0 ? void 0 : _a.dispose();
_this._locationSubs.delete(group.id);
}), {
dispose: function () {
_this._locationSubs.forEach(function (sub) { return sub.dispose(); });
_this._locationSubs.clear();
},
});
return _this;
}
LiveRegionService.prototype._trackLocation = function (group) {
var _this = this;
var prev = group.api.location.type;
var sub = group.api.onDidLocationChange(function (e) {
var next = e.location.type;
if (next === prev) {
return;
}
prev = next;
var panel = group.activePanel;
if (!panel) {
return;
}
var kind = next === 'floating'
? 'float'
: next === 'popout'
? 'popout'
: 'dock';
_this._announce(panel, kind);
});
this._locationSubs.set(group.id, sub);
};
LiveRegionService.prototype.announce = function (message, politeness) {
if (politeness === void 0) { politeness = 'polite'; }
// Opt-out (read live so `updateOptions({ announcements })` applies).
if (this._host.options.announcements === false ||
this._suppressDepth > 0 ||
!message) {
return;
}
// Apps can route announcements into their own SR system instead of the
// built-in regions (e.g. a shared app-wide live region).
var announcer = this._host.options.announcer;
if (announcer) {
announcer({ message: message, politeness: politeness });
return;
}
// Clearing first forces SRs to re-announce an identical message.
var region = politeness === 'assertive' ? this._assertive : this._polite;
region.textContent = '';
region.textContent = message;
};
LiveRegionService.prototype._announce = function (panel, kind) {
var _a, _b;
// The app may localise/override the message, suppress it (null / ''),
// or fall through to the default (undefined).
var custom = (_b = (_a = this._host.options).getAnnouncement) === null || _b === void 0 ? void 0 : _b.call(_a, { kind: kind, panel: panel });
if (custom === null || custom === '') {
return;
}
this.announce(custom !== null && custom !== void 0 ? custom : this._defaultMessage(panel, kind));
};
LiveRegionService.prototype._defaultMessage = function (panel, kind) {
var _a;
var m = (0, accessibilityMessages_1.resolveMessages)(this._host.options.messages);
var name = (_a = panel.title) !== null && _a !== void 0 ? _a : panel.id;
switch (kind) {
case 'open':
return m.panelOpened(name);
case 'close':
return m.panelClosed(name);
case 'maximize':
return m.groupMaximized(name);
case 'restore':
return m.groupRestored(name);
case 'float':
return m.groupFloated(name);
case 'dock':
return m.groupDocked(name);
case 'popout':
return m.groupPoppedOut(name);
}
};
return LiveRegionService;
}(lifecycle_1.CompositeDisposable));
exports.LiveRegionService = LiveRegionService;
exports.LiveRegionModule = (0, modules_1.defineModule)({
name: 'LiveRegion',
serviceKey: 'liveRegionService',
create: function (host) { return new LiveRegionService(host); },
});