Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

TypeScript - 'Element' is not assignable to HTMLInputElement

I am working with TypeScript and React. In my component, which is a dialog window, I want to store the trigger element, such as a button as a property. When the component unmounts, I want to return focus to that element. However, I am receiving an error from TSLint that I am not sure how to resolve.

class Dialog extends React.Component<Props, {}> {
  ...
  focusedElementBeforeDialogOpened: HTMLInputElement;

  componentDidMount() {
    this.focusedElementBeforeDialogOpened = document.activeElement;
    ...
  }
  ...
}

I am receiving the error on the line where I am assigning the property value:

[ts] Type 'Element' is not assignable to type 'HTMLInputElement'.
  Property 'accept' is missing in type 'Element'.

However, if I change the type of the property to Element or even HTMLInputElement I receive an error in componentWillUnmount()

componentWillUnmount() {
    ...
    this.focusedElementBeforeDialogOpened.focus();
}

This error is in regards to Element type not having the focus() method.

Question

Is there a way to tell TypeScript that document.activeElement should be an input type? Something like

this.focusedElementBeforeDialogOpened = <HTMLInputElement>document.activeElement;

Or is there a better way around this so declare the type that supports both document.activeElement and .focus()?

like image 847
Yuschick Avatar asked Nov 09 '17 09:11

Yuschick


2 Answers

From the docs of document.activeElement:

Often this will return an <input> or <textarea> object, if it has the text selection at the time. If so, you can get more detail by using the element's selectionStart and selectionEnd properties. Other times the focused element might be a <select> element (menu) or an <input> element, of type button, checkbox or radio.

That is, document.activeElement is not necessarily an instance of HTMLInputElement (it can also be a HTMLSelectElement, etc.).

If you are indeed only looking for a HTMLInputElement, you can use a simple instanceof type-guard, which is recognized by TypeScript:

componentDidMount() {
    if (document.activeElement instanceof HTMLInputElement) {
        this.focusedElementBeforeDialogOpened = document.activeElement;
    }
    ...
}

Additionally, you can detect if the element is focusable, e.g. by defining your own type-guard:

interface IFocusableElement {
    focus(): void;
}

function isFocusable(element: any): element is IFocusableElement {
    return (typeof element.focus === "function");
}

And then use IFocusableElement as the type of this.focusedElementBeforeDialogOpened and your own type guard:

focusedElementBeforeDialogOpened: IFocusableElement;

componentDidMount() {
    if (document.activeElement && isFocusable(document.activeElement)) {
        this.focusedElementBeforeDialogOpened = document.activeElement;
    }
    ...
}

If you also need the original API offered by Element, you can just use an intersection type:

focusedElementBeforeDialogOpened: IFocusableElement & Element;
like image 64
John Weisz Avatar answered Oct 16 '22 01:10

John Weisz


Indeed, the definitions in lib.es6.d.ts are a bit inconsistent, unless it's the W3C specifications that are a bit confusing:

interface Document extends [...] {
    /**
     * Gets the object that has the focus when the parent document has focus.
     */
    readonly activeElement: Element;
    [...]
    focus(): void;
    [...]
}

interface HTMLElement extends Element {
    [...]
    focus(): void;
    [...]
}

So, the 2 only kind of objects that we can focus manually are Document and HtmlElement, but any Element can be focused!

If you only need to give back the focus, you can do something like:

class Dialog extends React.Component<Props, {}> {
    private resetFocus = () => {};

    componentDidMount() {
        const elementToFocus = document.activeElement instanceof HTMLElement
            ? document.activeElement
            : document;
        this.resetFocus = () => elementToFocus.focus();
    }

    componentWillUnmount() {
        this.resetFocus();
    }
}

or

componentDidMount() {
    const activeElement = document.activeElement as any;
    if (typeof activeElement.focus === 'function') {
        this.resetFocus = () => activeElement.focus();
    }
}
like image 45
Romain Deneau Avatar answered Oct 16 '22 02:10

Romain Deneau