Skip to content

Keyborg instances are overriden when multiple code instances are deployed on the same site #99

Description

Issue Description

Keyborg uses a global __keyborg property on the window object to store its core instance and references to all Keyborg instances. When multiple Keyborg instances are created on the same site but from different modules, they override each other because they share the same global property but use different id counters. This happens because each module initializes its own private _lastId variable.

Code Reference

The issue occurs with the module-level counter that generates IDs:

let _lastId = 0;

Each instance uses this counter to create IDs in the constructor + it will register itself using the id on the global state:

keyborg/src/Keyborg.ts

Lines 293 to 302 in 76e5740

private constructor(win: WindowWithKeyborg, props?: KeyborgProps) {
this._id = "k" + ++_lastId;
this._win = win;
const current = win.__keyborg;
if (current) {
this._core = current.core;
current.refs[this._id] = this;
} else {

Impact

This issue causes significant problems in applications with multiple bundles that include Keyborg:

  • When multiple module instances are loaded, none are functioning properly as the override their Keyborg Instances in two way fashion
  • Previously created Keyborg instances stop working correctly
  • Keyboard navigation state tracking becomes inconsistent

Real-world impact: This is a critical issue for Microsoft Dynamics 365, where PCFs (Power Apps Component Framework) come with FluentUI v9 bundled. For example, a standard PCF with FluentUI v9 Combobox component will not indicate keyboard navigation mode when users interact with the component using the keyboard, because the Keyborg instance created by the PCF is overridden by the one created by the out-of-the-box script.

Proposed Solution

As there is already a concept for the global __keyborg property, we could extend it to store the next available ID for Keyborg instances. This way, each Keyborg instance would have a unique ID regardless of which module created it.

The solution would involve:

  1. Storing the last used ID in the global __keyborg object
  2. Using that shared counter instead of module-level _lastId
  3. Ensuring ID generation is synchronized across all Keyborg instances

I am happy to provide a PR for this issue if you agree with the suggested solution or have other ideas to address this problem.

Priority

This issue requires priority attention as it impacts Microsoft Dynamics 365 functionality and any other application that might bundle multiple instances of Keyborg.

Other Concerns

If there will come the time where the core class gets an major update even fix, everything will break.

This is due to the fact that the core instance is a singleton across all modules even if they are not the same version.

See:

keyborg/src/Keyborg.ts

Lines 299 to 308 in 76e5740

if (current) {
this._core = current.core;
current.refs[this._id] = this;
} else {
this._core = new KeyborgCore(win, props);
win.__keyborg = {
core: this._core,
refs: { [this._id]: this },
};
}

What ever the first keyborg instance is will provide the core functionality. This is concerning...

Workaround

I will provide a npm patch in the next days

Activity

  1. layershifter commented on May 16, 2025

    @layershifter
    Contributor
  2. mathis-m commented on May 16, 2025

    @mathis-m
    Author

    #75?

    Any reason why it still open?

    NPM Patch:

    (keyborg+2.6.0.patch)

    diff --git a/node_modules/keyborg/dist/esm/index.js b/node_modules/keyborg/dist/esm/index.js
    index 0b18026..2d18f5d 100644
    --- a/node_modules/keyborg/dist/esm/index.js
    +++ b/node_modules/keyborg/dist/esm/index.js
    @@ -216,7 +216,6 @@ function getLastFocusedProgrammatically(win) {
     
     // src/Keyborg.ts
     var _dismissTimeout = 500;
    -var _lastId = 0;
     var KeyborgCore = class {
       constructor(win, props) {
         this._isNavigatingWithKeyboard_DO_NOT_USE = false;
    @@ -266,7 +265,13 @@ var KeyborgCore = class {
             }
           }
         };
    -    this.id = "c" + ++_lastId;
    +    if (win?.__keyborg?.lastId !== undefined) {
    +      this.id = "c" + ++win.__keyborg.lastId;
    +    } else {
    +      win.__keyborg = win.__keyborg || {};
    +      win.__keyborg.lastId = 10001;
    +      this.id = "c10001";
    +    }
         this._win = win;
         const doc = win.document;
         if (props) {
    @@ -374,15 +379,19 @@ var KeyborgCore = class {
     var Keyborg = class _Keyborg {
       constructor(win, props) {
         this._cb = [];
    -    this._id = "k" + ++_lastId;
         this._win = win;
         const current = win.__keyborg;
         if (current) {
    +      current.lastId = current.lastId || 10000;
    +      this._id = "k" + ++current.lastId;
           this._core = current.core;
           current.refs[this._id] = this;
         } else {
           this._core = new KeyborgCore(win, props);
    +      let lastId = win.__keyborg?.lastId || 10000;
    +      this._id = "k" + ++lastId;
           win.__keyborg = {
    +        lastId: lastId,
             core: this._core,
             refs: { [this._id]: this }
           };
    diff --git a/node_modules/keyborg/dist/index.js b/node_modules/keyborg/dist/index.js
    index eb4bbe2..939a490 100644
    --- a/node_modules/keyborg/dist/index.js
    +++ b/node_modules/keyborg/dist/index.js
    @@ -247,7 +247,6 @@ function getLastFocusedProgrammatically(win) {
     
     // src/Keyborg.ts
     var _dismissTimeout = 500;
    -var _lastId = 0;
     var KeyborgCore = class {
       constructor(win, props) {
         this._isNavigatingWithKeyboard_DO_NOT_USE = false;
    @@ -297,7 +296,13 @@ var KeyborgCore = class {
             }
           }
         };
    -    this.id = "c" + ++_lastId;
    +    if (win?.__keyborg?.lastId !== undefined) {
    +      this.id = "c" + ++win.__keyborg.lastId;
    +    } else {
    +      win.__keyborg = win.__keyborg || {};
    +      win.__keyborg.lastId = 10001;
    +      this.id = "c10001";
    +    }
         this._win = win;
         const doc = win.document;
         if (props) {
    @@ -405,15 +410,19 @@ var KeyborgCore = class {
     var Keyborg = class _Keyborg {
       constructor(win, props) {
         this._cb = [];
    -    this._id = "k" + ++_lastId;
         this._win = win;
         const current = win.__keyborg;
         if (current) {
    +      current.lastId = current.lastId || 10000;
    +      this._id = "k" + ++current.lastId;
           this._core = current.core;
           current.refs[this._id] = this;
         } else {
           this._core = new KeyborgCore(win, props);
    +      let lastId = win.__keyborg?.lastId || 10000;
    +      this._id = "k" + ++lastId;
           win.__keyborg = {
    +        lastId: lastId,
             core: this._core,
             refs: { [this._id]: this }
           };
  3. added a commit that references this issue on Mar 4, 2026
    7695468
  4. added a commit that references this issue on Jun 19, 2026
    7b4cb62
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions