[ts] Type-check ui/Widget.js with TypeScript

This CL introduces a new 'WidgetElement' type. It is never
really constructed but it has the __widget and __widgetCounter
field. This way, TypeScript is able to understand
what is going on.

The rest of the CL are mostly added JSDoc annotation, casts and some
more explicit null checks that both TS and Closure understand.

CL 3 out of 3 to enable TS for ui/Widget.js.

Bug: chromium:1011811
Change-Id: I15f3918a25f3387d79ad06a68e79e427de5890e0
Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/2349174
Commit-Queue: Simon Zünd <szuend@chromium.org>
Reviewed-by: Jack Franklin <jacktfranklin@chromium.org>
Reviewed-by: Paul Lewis <aerotwist@chromium.org>
This commit is contained in:
Simon Zünd
2020-08-12 12:51:57 +00:00
committed by Commit Bot
parent 3b4747f0e2
commit 10ebfaf26d
2 changed files with 90 additions and 26 deletions
+7
View File
@@ -88,6 +88,10 @@ declare namespace Adb {
type NetworkDiscoveryConfig = string[];
}
interface Document {
deepActiveElement(): Element|null;
}
interface HTMLElement {
createChild(tagName: string, className?: string, content?: string): HTMLElement;
createSVGChild(childType: string, className?: string): HTMLElement;
@@ -96,6 +100,7 @@ interface HTMLElement {
interface Element {
createChild(tagName: string, className?: string, content?: string): Element;
createTextChild(text: string): Text;
hasFocus(): boolean;
removeChildren(): void;
}
@@ -106,6 +111,8 @@ interface Event {
interface Node {
getComponentSelection(): Selection|null;
hasSameShadowRoot(other: Node): boolean;
parentElementOrShadowHost(): Element|null;
traverseNextNode(stayWithin?: Node): Node|null;
}
declare function isEnterKey(event: Event): boolean;
+83 -26
View File
@@ -24,9 +24,6 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
// @ts-nocheck
// TODO(crbug.com/1011811): Enable TypeScript compiler checks
import * as Common from '../common/common.js';
import * as DOMExtension from '../dom_extension/dom_extension.js';
import {Constraints, Size} from './Geometry.js';
@@ -34,6 +31,17 @@ import {appendStyle} from './utils/append-style.js';
import {createShadowRootWithCoreStyles} from './utils/create-shadow-root-with-core-styles.js';
import {XWidget} from './XWidget.js';
export class WidgetElement extends HTMLDivElement { // eslint-disable-line no-unused-vars
constructor() {
super();
/** @type {?Widget} */
this.__widget;
/** @type {?number} */
this.__widgetCounter;
}
}
/**
* @unrestricted
*/
@@ -44,53 +52,76 @@ export class Widget extends Common.ObjectWrapper.ObjectWrapper {
*/
constructor(isWebComponent, delegatesFocus) {
super();
/** @type {!WidgetElement} */
this.element;
this.contentElement = document.createElement('div');
this.contentElement.classList.add('widget');
if (isWebComponent) {
this.element = document.createElement('div');
this.element = /** @type {!WidgetElement} */ (document.createElement('div'));
this.element.classList.add('vbox');
this.element.classList.add('flex-auto');
this._shadowRoot = createShadowRootWithCoreStyles(this.element, undefined, delegatesFocus);
this._shadowRoot.appendChild(this.contentElement);
} else {
this.element = this.contentElement;
this.element = /** @type {!WidgetElement} */ (this.contentElement);
}
this._isWebComponent = isWebComponent;
this.element.__widget = this;
this._visible = false;
this._isRoot = false;
this._isShowing = false;
/** @type {!Array<!Widget>} */
this._children = [];
this._hideOnDetach = false;
this._notificationDepth = 0;
this._invalidationsSuspended = 0;
this._defaultFocusedChild = null;
/** @type {?Widget} */
this._parentWidget = null;
}
/**
* @param {!WidgetElement} parentElement
* @param {!WidgetElement} childElement
*/
static _incrementWidgetCounter(parentElement, childElement) {
const count = (childElement.__widgetCounter || 0) + (childElement.__widget ? 1 : 0);
if (!count) {
return;
}
while (parentElement) {
parentElement.__widgetCounter = (parentElement.__widgetCounter || 0) + count;
parentElement = parentElement.parentElementOrShadowHost();
/** @type {?WidgetElement} */
let currentElement = parentElement;
while (currentElement) {
currentElement.__widgetCounter = (currentElement.__widgetCounter || 0) + count;
currentElement = parentWidgetElementOrShadowHost(currentElement);
}
}
/**
* @param {!WidgetElement} parentElement
* @param {!WidgetElement} childElement
*/
static _decrementWidgetCounter(parentElement, childElement) {
const count = (childElement.__widgetCounter || 0) + (childElement.__widget ? 1 : 0);
if (!count) {
return;
}
while (parentElement) {
parentElement.__widgetCounter -= count;
parentElement = parentElement.parentElementOrShadowHost();
/** @type {?WidgetElement} */
let currentElement = parentElement;
while (currentElement) {
if (currentElement.__widgetCounter) {
currentElement.__widgetCounter -= count;
}
currentElement = parentWidgetElementOrShadowHost(currentElement);
}
}
/**
* @param {*} condition
* @param {string} message
*/
static __assert(condition, message) {
if (!condition) {
throw new Error(message);
@@ -156,7 +187,7 @@ export class Widget extends Common.ObjectWrapper.ObjectWrapper {
* @return {boolean}
*/
_inNotification() {
return !!this._notificationDepth || (this._parentWidget && this._parentWidget._inNotification());
return !!this._notificationDepth || !!(this._parentWidget && this._parentWidget._inNotification());
}
_parentIsShowing() {
@@ -254,15 +285,18 @@ export class Widget extends Common.ObjectWrapper.ObjectWrapper {
if (!this._isRoot) {
// Update widget hierarchy.
let currentParent = parentElement;
/** @type {?WidgetElement} */
let currentParent = /** @type {?WidgetElement} */ (parentElement);
while (currentParent && !currentParent.__widget) {
currentParent = currentParent.parentElementOrShadowHost();
currentParent = parentWidgetElementOrShadowHost(currentParent);
}
if (!currentParent || !currentParent.__widget) {
throw new Error('Attempt to attach widget to orphan node');
}
Widget.__assert(currentParent, 'Attempt to attach widget to orphan node');
this._attach(currentParent.__widget);
}
this._showWidget(parentElement, insertBefore);
this._showWidget(/** @type {!WidgetElement} */ (parentElement), insertBefore);
}
/**
@@ -275,6 +309,7 @@ export class Widget extends Common.ObjectWrapper.ObjectWrapper {
if (this._parentWidget) {
this.detach();
}
/** @type {?Widget} */
this._parentWidget = parentWidget;
this._parentWidget._children.push(this);
this._isRoot = false;
@@ -284,18 +319,21 @@ export class Widget extends Common.ObjectWrapper.ObjectWrapper {
if (this._visible) {
return;
}
Widget.__assert(this.element.parentElement, 'Attempt to show widget that is not hidden using hideWidget().');
this._showWidget(/** @type {!Element} */ (this.element.parentElement), this.element.nextSibling);
if (!this.element.parentElement) {
throw new Error('Attempt to show widget that is not hidden using hideWidget().');
}
this._showWidget(/** @type {!WidgetElement} */ (this.element.parentElement), this.element.nextSibling);
}
/**
* @param {!Element} parentElement
* @param {!WidgetElement} parentElement
* @param {?Node=} insertBefore
*/
_showWidget(parentElement, insertBefore) {
/** @type {?WidgetElement} */
let currentParent = parentElement;
while (currentParent && !currentParent.__widget) {
currentParent = currentParent.parentElementOrShadowHost();
currentParent = parentWidgetElementOrShadowHost(currentParent);
}
if (this._isRoot) {
@@ -354,7 +392,7 @@ export class Widget extends Common.ObjectWrapper.ObjectWrapper {
*/
_hideWidget(removeFromDOM) {
this._visible = false;
const parentElement = this.element.parentElement;
const parentElement = /** @type {!WidgetElement} */ (this.element.parentElement);
if (this._parentIsShowing()) {
this._processWillHide();
@@ -393,7 +431,7 @@ export class Widget extends Common.ObjectWrapper.ObjectWrapper {
if (this._visible) {
this._hideWidget(removeFromDOM);
} else if (removeFromDOM && this.element.parentElement) {
const parentElement = this.element.parentElement;
const parentElement = /** @type {!WidgetElement} */ (this.element.parentElement);
// Force kick out from DOM.
Widget._decrementWidgetCounter(parentElement, this.element);
DOMExtension.DOMExtension.originalRemoveChild.call(parentElement, this.element);
@@ -468,15 +506,24 @@ export class Widget extends Common.ObjectWrapper.ObjectWrapper {
* @param {string} cssFile
*/
registerRequiredCSS(cssFile) {
appendStyle(this._isWebComponent ? this._shadowRoot : this.element, cssFile);
if (this._isWebComponent) {
appendStyle(/** @type {!DocumentFragment} */ (this._shadowRoot), cssFile);
} else {
appendStyle(this.element, cssFile);
}
}
printWidgetHierarchy() {
/** @type {!Array<string>} */
const lines = [];
this._collectWidgetHierarchy('', lines);
console.log(lines.join('\n')); // eslint-disable-line no-console
}
/**
* @param {string} prefix
* @param {!Array<string>} lines
*/
_collectWidgetHierarchy(prefix, lines) {
lines.push(prefix + '[' + this.element.className + ']' + (this._children.length ? ' {' : ''));
@@ -509,7 +556,7 @@ export class Widget extends Common.ObjectWrapper.ObjectWrapper {
return;
}
const element = this._defaultFocusedElement;
const element = /** @type {?HTMLElement} */ (this._defaultFocusedElement);
if (element) {
if (!element.hasFocus()) {
element.focus();
@@ -614,7 +661,7 @@ export class Widget extends Common.ObjectWrapper.ObjectWrapper {
const cached = this._cachedConstraints;
delete this._cachedConstraints;
const actual = this.constraints();
if (!actual.isEqual(cached) && this._parentWidget) {
if (!actual.isEqual(cached || null) && this._parentWidget) {
this._parentWidget.invalidateConstraints();
} else {
this.doLayout();
@@ -737,8 +784,10 @@ export class WidgetFocusRestorer {
* @param {!Widget} widget
*/
constructor(widget) {
/** @type {?Widget} */
this._widget = widget;
this._previous = widget.element.ownerDocument.deepActiveElement();
/** @type {?HTMLElement} */
this._previous = /** @type {?HTMLElement} */ (widget.element.ownerDocument.deepActiveElement());
widget.focus();
}
@@ -753,3 +802,11 @@ export class WidgetFocusRestorer {
this._widget = null;
}
}
/**
* @param {!WidgetElement} element
* @return {?WidgetElement}
*/
function parentWidgetElementOrShadowHost(element) {
return /** @type {?WidgetElement} */ (element.parentElementOrShadowHost());
}