]> BookStack Code Mirror - bookstack/commitdiff
TS: Converted dom and keyboard nav services 5259/head
authorDan Brown <redacted>
Fri, 11 Oct 2024 20:55:51 +0000 (21:55 +0100)
committerDan Brown <redacted>
Fri, 11 Oct 2024 20:55:51 +0000 (21:55 +0100)
28 files changed:
dev/build/esbuild.js
resources/js/components/add-remove-rows.js
resources/js/components/ajax-delete-row.js
resources/js/components/ajax-form.js
resources/js/components/attachments.js
resources/js/components/auto-suggest.js
resources/js/components/book-sort.js
resources/js/components/code-editor.js
resources/js/components/confirm-dialog.js
resources/js/components/dropdown.js
resources/js/components/dropzone.js
resources/js/components/entity-permissions.js
resources/js/components/entity-search.js
resources/js/components/entity-selector.js
resources/js/components/event-emit-select.js
resources/js/components/global-search.js
resources/js/components/image-manager.js
resources/js/components/optional-input.js
resources/js/components/page-comment.js
resources/js/components/page-comments.js
resources/js/components/page-display.js
resources/js/components/page-editor.js
resources/js/components/pointer.js
resources/js/components/popup.js
resources/js/components/template-manager.js
resources/js/components/user-select.js
resources/js/services/dom.ts [moved from resources/js/services/dom.js with 63% similarity]
resources/js/services/keyboard-navigation.ts [moved from resources/js/services/keyboard-navigation.js with 66% similarity]

index fea8c01e353887d2bb169d330fec1bc51b997a0a..cd8bf279f28db0858cbd3c238ea029d4582d107d 100644 (file)
@@ -10,7 +10,7 @@ const isProd = process.argv[2] === 'production';
 
 // Gather our input files
 const entryPoints = {
 
 // Gather our input files
 const entryPoints = {
-    app: path.join(__dirname, '../../resources/js/app.js'),
+    app: path.join(__dirname, '../../resources/js/app.ts'),
     code: path.join(__dirname, '../../resources/js/code/index.mjs'),
     'legacy-modes': path.join(__dirname, '../../resources/js/code/legacy-modes.mjs'),
     markdown: path.join(__dirname, '../../resources/js/markdown/index.mjs'),
     code: path.join(__dirname, '../../resources/js/code/index.mjs'),
     'legacy-modes': path.join(__dirname, '../../resources/js/code/legacy-modes.mjs'),
     markdown: path.join(__dirname, '../../resources/js/markdown/index.mjs'),
index 488654279b4e0cb558cf9d390c42bd6b7ffc4ebc..e7de15ae5fa03bd63a720508638cb91663374712 100644 (file)
@@ -1,4 +1,4 @@
-import {onChildEvent} from '../services/dom';
+import {onChildEvent} from '../services/dom.ts';
 import {uniqueId} from '../services/util.ts';
 import {Component} from './component';
 
 import {uniqueId} from '../services/util.ts';
 import {Component} from './component';
 
index aa2801f19e666c52ee9f1d738b71478ec105552a..6ed3deedf4dd6ed9e16d7f77b9a5edaa54bded8a 100644 (file)
@@ -1,4 +1,4 @@
-import {onSelect} from '../services/dom';
+import {onSelect} from '../services/dom.ts';
 import {Component} from './component';
 
 export class AjaxDeleteRow extends Component {
 import {Component} from './component';
 
 export class AjaxDeleteRow extends Component {
index 583dde5724424defb44906504ab4973a2fb88af5..de1a6db43a7e83cad11b8d783fb3db9be9de01fd 100644 (file)
@@ -1,4 +1,4 @@
-import {onEnterPress, onSelect} from '../services/dom';
+import {onEnterPress, onSelect} from '../services/dom.ts';
 import {Component} from './component';
 
 /**
 import {Component} from './component';
 
 /**
index f45b25e36d05f92d96d175e8b5207020bdcd4d14..2dc7313a880901014a08c2e2f9c7a163030ab2ea 100644 (file)
@@ -1,4 +1,4 @@
-import {showLoading} from '../services/dom';
+import {showLoading} from '../services/dom.ts';
 import {Component} from './component';
 
 export class Attachments extends Component {
 import {Component} from './component';
 
 export class Attachments extends Component {
index 07711312f1dfd85efbb1d842d92f1a3efd32d429..0b828e71bd1ea35976f348d02fd1c31dca2c5de6 100644 (file)
@@ -1,7 +1,7 @@
 import {escapeHtml} from '../services/util.ts';
 import {escapeHtml} from '../services/util.ts';
-import {onChildEvent} from '../services/dom';
+import {onChildEvent} from '../services/dom.ts';
 import {Component} from './component';
 import {Component} from './component';
-import {KeyboardNavigationHandler} from '../services/keyboard-navigation';
+import {KeyboardNavigationHandler} from '../services/keyboard-navigation.ts';
 
 const ajaxCache = {};
 
 
 const ajaxCache = {};
 
index 2ba7d5d36b00dc912be8c3d1f25f1b5e464d2ffe..48557141f6f7d80e66c501bfe9655340694566f0 100644 (file)
@@ -1,6 +1,6 @@
 import Sortable, {MultiDrag} from 'sortablejs';
 import {Component} from './component';
 import Sortable, {MultiDrag} from 'sortablejs';
 import {Component} from './component';
-import {htmlToDom} from '../services/dom';
+import {htmlToDom} from '../services/dom.ts';
 
 // Auto sort control
 const sortOperations = {
 
 // Auto sort control
 const sortOperations = {
index 091c3483f4d6bd1ecffabb7f52f3f0b17933abfa..12937d47293b31effb7e2766d68af08598834899 100644 (file)
@@ -1,4 +1,4 @@
-import {onChildEvent, onEnterPress, onSelect} from '../services/dom';
+import {onChildEvent, onEnterPress, onSelect} from '../services/dom.ts';
 import {Component} from './component';
 
 export class CodeEditor extends Component {
 import {Component} from './component';
 
 export class CodeEditor extends Component {
index 184618fccfad9928cca02f81f3450b61fbe21ebe..00f3cfed201c34016da201a0fca14afb6c07e37c 100644 (file)
@@ -1,4 +1,4 @@
-import {onSelect} from '../services/dom';
+import {onSelect} from '../services/dom.ts';
 import {Component} from './component';
 
 /**
 import {Component} from './component';
 
 /**
index 4efd428acf72b408dc4867e94a203173ed06c049..5dd5dd93b013023ebf466ef021e9237dd1b57ce7 100644 (file)
@@ -1,5 +1,5 @@
-import {onSelect} from '../services/dom';
-import {KeyboardNavigationHandler} from '../services/keyboard-navigation';
+import {onSelect} from '../services/dom.ts';
+import {KeyboardNavigationHandler} from '../services/keyboard-navigation.ts';
 import {Component} from './component';
 
 /**
 import {Component} from './component';
 
 /**
index 920fe875f22135de5a302dee0a8e44d7207aaf86..598e0d8d48b685cdb58a9782d5addc4eafa64732 100644 (file)
@@ -2,7 +2,7 @@ import {Component} from './component';
 import {Clipboard} from '../services/clipboard.ts';
 import {
     elem, getLoading, onSelect, removeLoading,
 import {Clipboard} from '../services/clipboard.ts';
 import {
     elem, getLoading, onSelect, removeLoading,
-} from '../services/dom';
+} from '../services/dom.ts';
 
 export class Dropzone extends Component {
 
 
 export class Dropzone extends Component {
 
index 7ab99a2a70bb2f45445d485b6aff49e018b25d7b..b020c5d85ba8f785785df1e2f98e2607f0e9b385 100644 (file)
@@ -1,4 +1,4 @@
-import {htmlToDom} from '../services/dom';
+import {htmlToDom} from '../services/dom.ts';
 import {Component} from './component';
 
 export class EntityPermissions extends Component {
 import {Component} from './component';
 
 export class EntityPermissions extends Component {
index 7a50444708dee6313ab5b2caee5a2dd21def7cdb..9d45133266d7e4095219970ecc22361d9368aa2d 100644 (file)
@@ -1,4 +1,4 @@
-import {onSelect} from '../services/dom';
+import {onSelect} from '../services/dom.ts';
 import {Component} from './component';
 
 export class EntitySearch extends Component {
 import {Component} from './component';
 
 export class EntitySearch extends Component {
index 561370d7a342004dacc592e81d5366a07e1d00a9..7491119a137ffbb24d0d83f0f4be1ec46add9ac7 100644 (file)
@@ -1,4 +1,4 @@
-import {onChildEvent} from '../services/dom';
+import {onChildEvent} from '../services/dom.ts';
 import {Component} from './component';
 
 /**
 import {Component} from './component';
 
 /**
index 2097c0528868181cdc9e94c67cc630fdfca18652..f722a25e71b75faebb784b0ddbc044af3099fd76 100644 (file)
@@ -1,4 +1,4 @@
-import {onSelect} from '../services/dom';
+import {onSelect} from '../services/dom.ts';
 import {Component} from './component';
 
 /**
 import {Component} from './component';
 
 /**
index 44c0d02f9d48a94e7c17f741cb9c92b5f5bc6d75..2cdaf591ac458d99c13c5d3f8638d86a52ef3141 100644 (file)
@@ -1,6 +1,6 @@
-import {htmlToDom} from '../services/dom';
+import {htmlToDom} from '../services/dom.ts';
 import {debounce} from '../services/util.ts';
 import {debounce} from '../services/util.ts';
-import {KeyboardNavigationHandler} from '../services/keyboard-navigation';
+import {KeyboardNavigationHandler} from '../services/keyboard-navigation.ts';
 import {Component} from './component';
 
 /**
 import {Component} from './component';
 
 /**
index 47231477b684fc00e1d453bbc5a83d88a3188500..c8108ab28c19f6b456e419a50833ae8cd98f9b34 100644 (file)
@@ -1,6 +1,6 @@
 import {
     onChildEvent, onSelect, removeLoading, showLoading,
 import {
     onChildEvent, onSelect, removeLoading, showLoading,
-} from '../services/dom';
+} from '../services/dom.ts';
 import {Component} from './component';
 
 export class ImageManager extends Component {
 import {Component} from './component';
 
 export class ImageManager extends Component {
index 64cee12cd29b6a212a2fb882be6a0be193598a46..1b133047d00d1d23d2bec486ee5ca7a84beb0af9 100644 (file)
@@ -1,4 +1,4 @@
-import {onSelect} from '../services/dom';
+import {onSelect} from '../services/dom.ts';
 import {Component} from './component';
 
 export class OptionalInput extends Component {
 import {Component} from './component';
 
 export class OptionalInput extends Component {
index fd8ad1f2e47b0ac5a5c6494f7ffcc9c5b2df5d8e..8c0a8b33e5406a4659a8ff7cfe97407a1acc6df8 100644 (file)
@@ -1,5 +1,5 @@
 import {Component} from './component';
 import {Component} from './component';
-import {getLoading, htmlToDom} from '../services/dom';
+import {getLoading, htmlToDom} from '../services/dom.ts';
 import {buildForInput} from '../wysiwyg-tinymce/config';
 
 export class PageComment extends Component {
 import {buildForInput} from '../wysiwyg-tinymce/config';
 
 export class PageComment extends Component {
index 1d6abfe2044ff3a0a9e006f29e04fb4fef94601c..3d7e1365f30946e5d577787b6f251e306496900e 100644 (file)
@@ -1,5 +1,5 @@
 import {Component} from './component';
 import {Component} from './component';
-import {getLoading, htmlToDom} from '../services/dom';
+import {getLoading, htmlToDom} from '../services/dom.ts';
 import {buildForInput} from '../wysiwyg-tinymce/config';
 
 export class PageComments extends Component {
 import {buildForInput} from '../wysiwyg-tinymce/config';
 
 export class PageComments extends Component {
index ff9d68c7a5a245bfd0d0712b34d6cbdf4046b1c7..d3ac78a4ad19347779583d1331cb470d6532959a 100644 (file)
@@ -1,4 +1,4 @@
-import * as DOM from '../services/dom';
+import * as DOM from '../services/dom.ts';
 import {scrollAndHighlightElement} from '../services/util.ts';
 import {Component} from './component';
 
 import {scrollAndHighlightElement} from '../services/util.ts';
 import {Component} from './component';
 
index 9450444ca41eadf7e7eac2e83814160712f9025d..7ffceb0f9048a281f53ae82dfe31732e52d17247 100644 (file)
@@ -1,4 +1,4 @@
-import {onSelect} from '../services/dom';
+import {onSelect} from '../services/dom.ts';
 import {debounce} from '../services/util.ts';
 import {Component} from './component';
 import {utcTimeStampToLocalTime} from '../services/dates.ts';
 import {debounce} from '../services/util.ts';
 import {Component} from './component';
 import {utcTimeStampToLocalTime} from '../services/dates.ts';
index 607576cb9f9595a6cb9382c44e6c8a7f6002cba4..292b923e5519f47742990918214c7f46b7e9140b 100644 (file)
@@ -1,4 +1,4 @@
-import * as DOM from '../services/dom';
+import * as DOM from '../services/dom.ts';
 import {Component} from './component';
 import {copyTextToClipboard} from '../services/clipboard.ts';
 
 import {Component} from './component';
 import {copyTextToClipboard} from '../services/clipboard.ts';
 
index edd428037334c64ecec9d1dcb9a22621184dc4c8..6bd8f9c722b7044b9ae9ab5cd73867e8383a5434 100644 (file)
@@ -1,5 +1,5 @@
 import {fadeIn, fadeOut} from '../services/animations.ts';
 import {fadeIn, fadeOut} from '../services/animations.ts';
-import {onSelect} from '../services/dom';
+import {onSelect} from '../services/dom.ts';
 import {Component} from './component';
 
 /**
 import {Component} from './component';
 
 /**
index 56ec876d48bbb66a78e65038e4bfdd45a09759d5..cf81990ab3f934539bdc104a3226d2575e745d9e 100644 (file)
@@ -1,4 +1,4 @@
-import * as DOM from '../services/dom';
+import * as DOM from '../services/dom.ts';
 import {Component} from './component';
 
 export class TemplateManager extends Component {
 import {Component} from './component';
 
 export class TemplateManager extends Component {
index e6adc3c23c82d4e163c12c4f4c42adfc1d80d249..f9ec03ed366a4c6ea4bd643dea0ab7e524ff5ae7 100644 (file)
@@ -1,4 +1,4 @@
-import {onChildEvent} from '../services/dom';
+import {onChildEvent} from '../services/dom.ts';
 import {Component} from './component';
 
 export class UserSelect extends Component {
 import {Component} from './component';
 
 export class UserSelect extends Component {
similarity index 63%
rename from resources/js/services/dom.js
rename to resources/js/services/dom.ts
index bcfd0b565da2ab9b9144af2243d2cdb2fe13cc50..c88827bac40a1788b89295152799b62e9871425f 100644 (file)
@@ -1,12 +1,15 @@
+/**
+ * Check if the given param is a HTMLElement
+ */
+export function isHTMLElement(el: any): el is HTMLElement {
+    return el instanceof HTMLElement;
+}
+
 /**
  * Create a new element with the given attrs and children.
  * Children can be a string for text nodes or other elements.
 /**
  * Create a new element with the given attrs and children.
  * Children can be a string for text nodes or other elements.
- * @param {String} tagName
- * @param {Object<String, String>} attrs
- * @param {Element[]|String[]}children
- * @return {*}
  */
  */
-export function elem(tagName, attrs = {}, children = []) {
+export function elem(tagName: string, attrs: Record<string, string> = {}, children: Element[]|string[] = []): HTMLElement {
     const el = document.createElement(tagName);
 
     for (const [key, val] of Object.entries(attrs)) {
     const el = document.createElement(tagName);
 
     for (const [key, val] of Object.entries(attrs)) {
@@ -30,10 +33,8 @@ export function elem(tagName, attrs = {}, children = []) {
 
 /**
  * Run the given callback against each element that matches the given selector.
 
 /**
  * Run the given callback against each element that matches the given selector.
- * @param {String} selector
- * @param {Function<Element>} callback
  */
  */
-export function forEach(selector, callback) {
+export function forEach(selector: string, callback: (el: Element) => any) {
     const elements = document.querySelectorAll(selector);
     for (const element of elements) {
         callback(element);
     const elements = document.querySelectorAll(selector);
     for (const element of elements) {
         callback(element);
@@ -42,11 +43,8 @@ export function forEach(selector, callback) {
 
 /**
  * Helper to listen to multiple DOM events
 
 /**
  * Helper to listen to multiple DOM events
- * @param {Element} listenerElement
- * @param {Array<String>} events
- * @param {Function<Event>} callback
  */
  */
-export function onEvents(listenerElement, events, callback) {
+export function onEvents(listenerElement: Element, events: string[], callback: (e: Event) => any): void {
     for (const eventName of events) {
         listenerElement.addEventListener(eventName, callback);
     }
     for (const eventName of events) {
         listenerElement.addEventListener(eventName, callback);
     }
@@ -55,10 +53,8 @@ export function onEvents(listenerElement, events, callback) {
 /**
  * Helper to run an action when an element is selected.
  * A "select" is made to be accessible, So can be a click, space-press or enter-press.
 /**
  * Helper to run an action when an element is selected.
  * A "select" is made to be accessible, So can be a click, space-press or enter-press.
- * @param {HTMLElement|Array} elements
- * @param {function} callback
  */
  */
-export function onSelect(elements, callback) {
+export function onSelect(elements: HTMLElement|HTMLElement[], callback: (e: Event) => any): void {
     if (!Array.isArray(elements)) {
         elements = [elements];
     }
     if (!Array.isArray(elements)) {
         elements = [elements];
     }
@@ -76,16 +72,13 @@ export function onSelect(elements, callback) {
 
 /**
  * Listen to key press on the given element(s).
 
 /**
  * Listen to key press on the given element(s).
- * @param {String} key
- * @param {HTMLElement|Array} elements
- * @param {function} callback
  */
  */
-function onKeyPress(key, elements, callback) {
+function onKeyPress(key: string, elements: HTMLElement|HTMLElement[], callback: (e: KeyboardEvent) => any): void {
     if (!Array.isArray(elements)) {
         elements = [elements];
     }
 
     if (!Array.isArray(elements)) {
         elements = [elements];
     }
 
-    const listener = event => {
+    const listener = (event: KeyboardEvent) => {
         if (event.key === key) {
             callback(event);
         }
         if (event.key === key) {
             callback(event);
         }
@@ -96,19 +89,15 @@ function onKeyPress(key, elements, callback) {
 
 /**
  * Listen to enter press on the given element(s).
 
 /**
  * Listen to enter press on the given element(s).
- * @param {HTMLElement|Array} elements
- * @param {function} callback
  */
  */
-export function onEnterPress(elements, callback) {
+export function onEnterPress(elements: HTMLElement|HTMLElement[], callback: (e: KeyboardEvent) => any): void {
     onKeyPress('Enter', elements, callback);
 }
 
 /**
  * Listen to escape press on the given element(s).
     onKeyPress('Enter', elements, callback);
 }
 
 /**
  * Listen to escape press on the given element(s).
- * @param {HTMLElement|Array} elements
- * @param {function} callback
  */
  */
-export function onEscapePress(elements, callback) {
+export function onEscapePress(elements: HTMLElement|HTMLElement[], callback: (e: KeyboardEvent) => any): void {
     onKeyPress('Escape', elements, callback);
 }
 
     onKeyPress('Escape', elements, callback);
 }
 
@@ -116,14 +105,15 @@ export function onEscapePress(elements, callback) {
  * Set a listener on an element for an event emitted by a child
  * matching the given childSelector param.
  * Used in a similar fashion to jQuery's $('listener').on('eventName', 'childSelector', callback)
  * Set a listener on an element for an event emitted by a child
  * matching the given childSelector param.
  * Used in a similar fashion to jQuery's $('listener').on('eventName', 'childSelector', callback)
- * @param {Element} listenerElement
- * @param {String} childSelector
- * @param {String} eventName
- * @param {Function} callback
  */
  */
-export function onChildEvent(listenerElement, childSelector, eventName, callback) {
-    listenerElement.addEventListener(eventName, event => {
-        const matchingChild = event.target.closest(childSelector);
+export function onChildEvent(
+    listenerElement: HTMLElement,
+    childSelector: string,
+    eventName: string,
+    callback: (this: HTMLElement, e: Event, child: HTMLElement) => any
+): void {
+    listenerElement.addEventListener(eventName, (event: Event) => {
+        const matchingChild = (event.target as HTMLElement|null)?.closest(childSelector) as HTMLElement;
         if (matchingChild) {
             callback.call(matchingChild, event, matchingChild);
         }
         if (matchingChild) {
             callback.call(matchingChild, event, matchingChild);
         }
@@ -132,16 +122,13 @@ export function onChildEvent(listenerElement, childSelector, eventName, callback
 
 /**
  * Look for elements that match the given selector and contain the given text.
 
 /**
  * Look for elements that match the given selector and contain the given text.
- * Is case insensitive and returns the first result or null if nothing is found.
- * @param {String} selector
- * @param {String} text
- * @returns {Element}
+ * Is case-insensitive and returns the first result or null if nothing is found.
  */
  */
-export function findText(selector, text) {
+export function findText(selector: string, text: string): Element|null {
     const elements = document.querySelectorAll(selector);
     text = text.toLowerCase();
     for (const element of elements) {
     const elements = document.querySelectorAll(selector);
     text = text.toLowerCase();
     for (const element of elements) {
-        if (element.textContent.toLowerCase().includes(text)) {
+        if ((element.textContent || '').toLowerCase().includes(text) && isHTMLElement(element)) {
             return element;
         }
     }
             return element;
         }
     }
@@ -151,17 +138,15 @@ export function findText(selector, text) {
 /**
  * Show a loading indicator in the given element.
  * This will effectively clear the element.
 /**
  * Show a loading indicator in the given element.
  * This will effectively clear the element.
- * @param {Element} element
  */
  */
-export function showLoading(element) {
+export function showLoading(element: HTMLElement): void {
     element.innerHTML = '<div class="loading-container"><div></div><div></div><div></div></div>';
 }
 
 /**
  * Get a loading element indicator element.
     element.innerHTML = '<div class="loading-container"><div></div><div></div><div></div></div>';
 }
 
 /**
  * Get a loading element indicator element.
- * @returns {Element}
  */
  */
-export function getLoading() {
+export function getLoading(): HTMLElement {
     const wrap = document.createElement('div');
     wrap.classList.add('loading-container');
     wrap.innerHTML = '<div></div><div></div><div></div>';
     const wrap = document.createElement('div');
     wrap.classList.add('loading-container');
     wrap.innerHTML = '<div></div><div></div><div></div>';
@@ -170,9 +155,8 @@ export function getLoading() {
 
 /**
  * Remove any loading indicators within the given element.
 
 /**
  * Remove any loading indicators within the given element.
- * @param {Element} element
  */
  */
-export function removeLoading(element) {
+export function removeLoading(element: HTMLElement): void {
     const loadingEls = element.querySelectorAll('.loading-container');
     for (const el of loadingEls) {
         el.remove();
     const loadingEls = element.querySelectorAll('.loading-container');
     for (const el of loadingEls) {
         el.remove();
@@ -182,12 +166,15 @@ export function removeLoading(element) {
 /**
  * Convert the given html data into a live DOM element.
  * Initiates any components defined in the data.
 /**
  * Convert the given html data into a live DOM element.
  * Initiates any components defined in the data.
- * @param {String} html
- * @returns {Element}
  */
  */
-export function htmlToDom(html) {
+export function htmlToDom(html: string): HTMLElement {
     const wrap = document.createElement('div');
     wrap.innerHTML = html;
     window.$components.init(wrap);
     const wrap = document.createElement('div');
     wrap.innerHTML = html;
     window.$components.init(wrap);
-    return wrap.children[0];
+    const firstChild = wrap.children[0];
+    if (!isHTMLElement(firstChild)) {
+        throw new Error('Could not find child HTMLElement when creating DOM element from HTML');
+    }
+
+    return firstChild;
 }
 }
similarity index 66%
rename from resources/js/services/keyboard-navigation.js
rename to resources/js/services/keyboard-navigation.ts
index 34111bb2d37886e3d98a7e150fd7ada800ba186b..13fbdfecc9d81561bec3ef7ce183e605f914fce7 100644 (file)
@@ -1,14 +1,17 @@
+import {isHTMLElement} from "./dom";
+
+type OptionalKeyEventHandler = ((e: KeyboardEvent) => any)|null;
+
 /**
  * Handle common keyboard navigation events within a given container.
  */
 export class KeyboardNavigationHandler {
 
 /**
  * Handle common keyboard navigation events within a given container.
  */
 export class KeyboardNavigationHandler {
 
-    /**
-     * @param {Element} container
-     * @param {Function|null} onEscape
-     * @param {Function|null} onEnter
-     */
-    constructor(container, onEscape = null, onEnter = null) {
+    protected containers: HTMLElement[];
+    protected onEscape: OptionalKeyEventHandler;
+    protected onEnter: OptionalKeyEventHandler;
+
+    constructor(container: HTMLElement, onEscape: OptionalKeyEventHandler = null, onEnter: OptionalKeyEventHandler = null) {
         this.containers = [container];
         this.onEscape = onEscape;
         this.onEnter = onEnter;
         this.containers = [container];
         this.onEscape = onEscape;
         this.onEnter = onEnter;
@@ -18,9 +21,8 @@ export class KeyboardNavigationHandler {
     /**
      * Also share the keyboard event handling to the given element.
      * Only elements within the original container are considered focusable though.
     /**
      * Also share the keyboard event handling to the given element.
      * Only elements within the original container are considered focusable though.
-     * @param {Element} element
      */
      */
-    shareHandlingToEl(element) {
+    shareHandlingToEl(element: HTMLElement) {
         this.containers.push(element);
         element.addEventListener('keydown', this.#keydownHandler.bind(this));
     }
         this.containers.push(element);
         element.addEventListener('keydown', this.#keydownHandler.bind(this));
     }
@@ -30,7 +32,8 @@ export class KeyboardNavigationHandler {
      */
     focusNext() {
         const focusable = this.#getFocusable();
      */
     focusNext() {
         const focusable = this.#getFocusable();
-        const currentIndex = focusable.indexOf(document.activeElement);
+        const activeEl = document.activeElement;
+        const currentIndex = isHTMLElement(activeEl) ? focusable.indexOf(activeEl) : -1;
         let newIndex = currentIndex + 1;
         if (newIndex >= focusable.length) {
             newIndex = 0;
         let newIndex = currentIndex + 1;
         if (newIndex >= focusable.length) {
             newIndex = 0;
@@ -44,7 +47,8 @@ export class KeyboardNavigationHandler {
      */
     focusPrevious() {
         const focusable = this.#getFocusable();
      */
     focusPrevious() {
         const focusable = this.#getFocusable();
-        const currentIndex = focusable.indexOf(document.activeElement);
+        const activeEl = document.activeElement;
+        const currentIndex = isHTMLElement(activeEl) ? focusable.indexOf(activeEl) : -1;
         let newIndex = currentIndex - 1;
         if (newIndex < 0) {
             newIndex = focusable.length - 1;
         let newIndex = currentIndex - 1;
         if (newIndex < 0) {
             newIndex = focusable.length - 1;
@@ -53,12 +57,9 @@ export class KeyboardNavigationHandler {
         focusable[newIndex].focus();
     }
 
         focusable[newIndex].focus();
     }
 
-    /**
-     * @param {KeyboardEvent} event
-     */
-    #keydownHandler(event) {
+    #keydownHandler(event: KeyboardEvent) {
         // Ignore certain key events in inputs to allow text editing.
         // Ignore certain key events in inputs to allow text editing.
-        if (event.target.matches('input') && (event.key === 'ArrowRight' || event.key === 'ArrowLeft')) {
+        if (isHTMLElement(event.target) && event.target.matches('input') && (event.key === 'ArrowRight' || event.key === 'ArrowLeft')) {
             return;
         }
 
             return;
         }
 
@@ -71,7 +72,7 @@ export class KeyboardNavigationHandler {
         } else if (event.key === 'Escape') {
             if (this.onEscape) {
                 this.onEscape(event);
         } else if (event.key === 'Escape') {
             if (this.onEscape) {
                 this.onEscape(event);
-            } else if (document.activeElement) {
+            } else if (isHTMLElement(document.activeElement)) {
                 document.activeElement.blur();
             }
         } else if (event.key === 'Enter' && this.onEnter) {
                 document.activeElement.blur();
             }
         } else if (event.key === 'Enter' && this.onEnter) {
@@ -81,14 +82,15 @@ export class KeyboardNavigationHandler {
 
     /**
      * Get an array of focusable elements within the current containers.
 
     /**
      * Get an array of focusable elements within the current containers.
-     * @returns {Element[]}
      */
      */
-    #getFocusable() {
-        const focusable = [];
+    #getFocusable(): HTMLElement[] {
+        const focusable: HTMLElement[] = [];
         const selector = '[tabindex]:not([tabindex="-1"]),[href],button:not([tabindex="-1"],[disabled]),input:not([type=hidden])';
         for (const container of this.containers) {
         const selector = '[tabindex]:not([tabindex="-1"]),[href],button:not([tabindex="-1"],[disabled]),input:not([type=hidden])';
         for (const container of this.containers) {
-            focusable.push(...container.querySelectorAll(selector));
+            const toAdd = [...container.querySelectorAll(selector)].filter(e => isHTMLElement(e));
+            focusable.push(...toAdd);
         }
         }
+
         return focusable;
     }
 
         return focusable;
     }