Skip to content

refactor(bundle-size): extract dummy infrastructure + auto-install via getRootDummyInputs#548

Open
layershifter wants to merge 19 commits into
microsoft:masterfrom
layershifter:chore/bundle-size-stack-11
Open

refactor(bundle-size): extract dummy infrastructure + auto-install via getRootDummyInputs#548
layershifter wants to merge 19 commits into
microsoft:masterfrom
layershifter:chore/bundle-size-stack-11

Conversation

@layershifter
Copy link
Copy Markdown
Member

Summary

Extracts all dummy-input infrastructure into its own modules and wires it up as opt-in via getRootDummyInputs(tabster):

  • RootDummyManager.ts — formerly inline in Root.ts.
  • MoverDummyManager.ts, GroupperDummyManager.ts, ModalizerDummyManager.ts — formerly inline per-feature dummy logic.
  • getRootDummyInputs(tabster) — the public opt-in. Registers the root factory, creates _dummyObserver, installs moveOutOfRoot (phantom-dummy routing), and (when controlTab or rootDummyInputs is on) calls root.addDummyInputs().

To preserve the existing public contract (controlTab: true is the documented default), createTabster auto-invokes getRootDummyInputs when controlTab=true. Consumers can pass controlTab: false to opt out entirely — in that mode, the dummy code can be tree-shaken when no feature needs phantom-dummy routing.

Per-feature dummy managers are direct-imported in Mover/Groupper/Modalizer, gated on tabster._dummyObserver.

Also drops _forceDummy bookkeeping from Root.

Includes the getModalizerWithDummyInputs bundle-size fixture (deferred from #538 because it imports the new getRootDummyInputs export).

Stack context

Stacked on top of #547. Net new in this PR: the dummy infrastructure extraction + the auto-install wiring + _forceDummy removal + the deferred fixture.

layershifter and others added 19 commits May 11, 2026 11:41
\`patch-package\` postinstall hook applies three changes to
keyborg@2.14.0 covering both the ESM (\`dist/index.js\`) and CJS
(\`dist/index.cjs\`) bundles:

1. \`event.details = details\` — drop the \`@deprecated\` alias of
   \`event.detail\`. Tabster reads \`e.detail\` exclusively (verified
   across src/State/FocusedElement.ts and the rest of the codebase).

2. \`triggerKeys\` / \`dismissKeys\` props + the supporting
   \`shouldDismiss\` / \`scheduleDismiss\` / \`dismissTimer\`
   machinery. Tabster only ever calls \`createKeyborg(getWindow())\`
   with no props.

3. \`canOverrideNativeFocus\` runtime probe. Replaces the
   \`_canOverrideNativeFocus\` flag with the implicit-true assumption
   modern browsers (everything since IE9) already satisfy. The
   conditional \`details.isFocusedProgrammatically\` write becomes
   unconditional — semantically identical when override works.

Bundle deltas (createTabster default-mode):
  keyborg slice: 3.71 → 3.12 kB (-590 B, -16%)
  createTabster:  30.78 → 30.18 kB (-600 B)
  getModalizer:   38.47 → 37.87 kB (-600 B)
  getMover:       44.54 → 43.94 kB (-600 B)
  getCrossOrigin: 89.64 → 89.04 kB (-600 B)
  allExports:     92.09 → 91.50 kB (-590 B)

Tests pass: 3 pre-existing failures, no regressions across default,
uncontrolled, and root-dummy-inputs modes.

Stop-gap until upstream microsoft/keyborg can release the same
trims (the changes belong there, not as a Tabster-side fork).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Converts the `Subscribable<A, B>` abstract class and its consumers to
factory functions returning plain objects.

- `Subscribable` → `createSubscribable<A,B>()` factory returning a
  `SubscribableCore` interface. The public surface (subscribe/
  subscribeFirst/unsubscribe) matches `Types.Subscribable`; setVal/
  getVal/trigger are exposed for the composing factory only.
- `KeyboardNavigationState` class → `createKeyboardNavigationState`
  factory.
- `FocusedElementState` class → `createFocusedElementState` factory.
  `FocusedElementState` is preserved as a const namespace for the
  `forgetMemorized` and `findNextTabbable` static helpers.
- `ObservedElementAPI` class → `createObservedElementAPI` factory.
- `CrossOriginFocusedElementState` class →
  `createCrossOriginFocusedElementState` factory, with `setVal` static
  kept under same-named const namespace.
- `CrossOriginObservedElementState` class →
  `createCrossOriginObservedElementState` factory, with `trigger` static
  kept under same-named const namespace.

Also trims `Subscribable` itself: drops the `callCallbacks` helper in
favour of the inlined `trigger` closure, and adds `declare` to
`TabsterCustomEvent.details` to drop the redundant initializer SWC was
emitting.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
DummyInput was 23.6% of the core bundle (the single largest module).
Converting the four classes here in one coordinated pass:

- DummyInput            → createDummyInput
- DummyInputManager     → createDummyInputManager (interface + factory),
                          with externally-used statics moveWithPhantomDummy
                          and addPhantomDummyWithTarget kept under a same-
                          named const namespace
- DummyInputManagerCore → createDummyInputManagerCore. The class's
                          constructor-return-existing-instance trick (used
                          when an element already has dummy inputs from
                          another subsystem) becomes a natural early-return
                          in a factory.
- DummyInputObserver    → createDummyInputObserver

The four DummyManager subclasses (RootDummyManager, ModalizerDummyManager,
MoverDummyManager, GroupperDummyManager) are migrated from `extends
DummyInputManager` to composing `createDummyInputManager` and calling
`manager.setHandlers(...)` directly. They become free functions
(createRootDummyManager etc.) returning the same DummyInputManager
interface, so the consuming `dummyManager` field on Root/Modalizer/Mover/
Groupper is now typed as the interface rather than the subclass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Class scaffolding (constructor + private field declarations + this-binding
on arrow methods) doesn't mangle as well as plain top-level functions and
closures. Switching RestorerAPI to a factory shaves ~570 B minified /
~45 B gzipped on getRestorer.

Pattern: closures replace private fields; the returned object exposes
only the public methods the interface requires.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Static methods (getDeloser, getHistory, forceRestoreFocus) are kept
under a same-named const namespace, so external call sites in
CrossOrigin.ts continue to work unchanged. The internal state needed
by those statics is exposed via an internal interface with _-prefixed
accessors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…actory conversions

Apply post-cherry-pick fixes across all 8 converted API factories:
- Use addListener/removeListener helpers (PR4) instead of direct addEventListener
- Use focusedElementState method calls instead of missing free-function exports
- Expose _grouppers/_modalizers as internal getters for test compatibility
- Add _grouppers and _modalizers to GroupperAPIInternal/ModalizerAPIInternal types
- Exclude generated bundle-size/.readable/ and .claude/ from prettier checks

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…urface

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…for tree-shaking

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown

📊 Bundle size report

Package & Exports Baseline (minified/GZIP) PR Change
tabster
all exports
113.84 kB
30.937 kB
93.848 kB
28.791 kB
-19.992 kB
-2.146 kB
tabster
createTabster (core)
39.761 kB
11.892 kB
29.793 kB
10.136 kB
-9.968 kB
-1.756 kB
tabster
findAllFocusable
0 B
0 B
29.861 kB
10.157 kB
🆕 New entry
tabster
findLastFocusable
0 B
0 B
29.85 kB
10.159 kB
🆕 New entry
tabster
findNextFocusable
0 B
0 B
29.836 kB
10.151 kB
🆕 New entry
tabster
findPrevFocusable
0 B
0 B
29.85 kB
10.157 kB
🆕 New entry
tabster
getCrossOrigin
110.623 kB
30.244 kB
91.366 kB
28.072 kB
-19.257 kB
-2.172 kB
tabster
getDeloser
49.268 kB
14.234 kB
38.548 kB
12.591 kB
-10.72 kB
-1.643 kB
tabster
getGroupper
47 kB
13.593 kB
37.106 kB
12.235 kB
-9.894 kB
-1.358 kB
tabster
getModalizer
49.072 kB
14.378 kB
38.379 kB
12.683 kB
-10.693 kB
-1.695 kB
tabster
getModalizer (with dummy inputs)
0 B
0 B
38.387 kB
12.685 kB
🆕 New entry
tabster
getMover
54.653 kB
15.887 kB
44.042 kB
14.327 kB
-10.611 kB
-1.56 kB
tabster
getObservedElement
45.564 kB
13.484 kB
34.278 kB
11.636 kB
-11.286 kB
-1.848 kB
tabster
getOutline
48.867 kB
14.216 kB
36.225 kB
12.109 kB
-12.642 kB
-2.107 kB
tabster
getRestorer
42.518 kB
12.52 kB
32.214 kB
10.866 kB
-10.304 kB
-1.654 kB

🤖 This report was generated against a579ebbd50e37f1565551549fe57bbc9ddafab64

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant