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()
?
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;
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();
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With