ai / keyux

JS library to improve keyboard UI of web apps
https://ai.github.io/keyux/
MIT License
391 stars 18 forks source link

Scope the event handlers to the specific DOM element #22

Open nd0ut opened 2 months ago

nd0ut commented 2 months ago

Hey there!

We're developing some third-party widgets and decided to integrate KeyUX with them. However, we encountered an issue where KeyUX affects the host page, which is unintended behavior. To scope KeyUX to our widget's DOM elements, we wrap the window object and apply filtering within it, like this:

/**
 * @typedef {Parameters<import('keyux').KeyUXModule>[0]} MinimalWindow
 */

/**
 * @implements {MinimalWindow}
 */
class ScopedMinimalWindow {
  /**
   * @private
   * @type {Map<Function, (event: Event) => void>}
   */
  _listeners = new Map();

  /**
   * @private
   * @type {Node[]}
   */
  _scope = [];

  /**
   * @param {'keydown' | 'keyup'} type
   * @param {(event: Event) => void} listener
   */
  addEventListener(type, listener) {
    /** @param {Event} e */
    const wrappedListener = (e) => {
      const target = e.target;
      if (!target) return;
      if (
        this._scope.some(
          (el) => el === e.target || el.contains(/** @type {Node} */ (target))
        )
      ) {
        listener(e);
      }
    };
    this._listeners.set(listener, wrappedListener);
    window.addEventListener(type, wrappedListener);
  }

  /**
   * @param {'keydown' | 'keyup'} type
   * @param {(event: {}) => void} listener
   */
  removeEventListener(type, listener) {
    const wrappedListener = this._listeners.get(listener);
    if (wrappedListener) {
      window.removeEventListener(type, wrappedListener);
    }
    this._listeners.delete(listener);
  }

  get document() {
    return window.document;
  }

  get navigator() {
    return window.navigator;
  }

  /** @param {Node} scope */
  registerScope(scope) {
    this._scope.push(scope);
  }

  destroy() {
    this._scope = [];
    this._listeners.forEach((listener, originalListener) => {
      window.removeEventListener("keydown", listener);
      window.removeEventListener("keyup", listener);
      this._listeners.delete(originalListener);
    });
  }
}

This approach works perfectly. However, I would like to ask your opinion on implementing scoping directly within KeyUX.

What do you think about this?

ai commented 2 months ago

I like the idea. Can you create PR which increase JS bundle size (if you plan to change default exports) only by 10-20% (or by using extra export)?