Skip to content

UI Events Helper

The ctx.ui.events object provides reactive subscriptions to Lumiverse UI state (keyboard, drawer, settings) and a generic DOM event delegation helper. This is particularly useful for tracking mobile keyboard dimensions and for binding interactions on extension-injected or mounted DOM elements without leaking global event listeners.

Keyboard State

Access the virtual keyboard presence and safe-area inset (especially on iOS PWA or Android Chrome where the window resizing or visual viewport varies).

export interface SpindleUIKeyboardState {
  /** True when the host believes a virtual keyboard is currently visible. */
  visible: boolean;
  /** Safe bottom inset in CSS pixels that keeps content above the keyboard. */
  insetBottom: number;
  /** Current visual viewport width in CSS pixels. */
  viewportWidth: number;
  /** Current visual viewport height in CSS pixels. */
  viewportHeight: number;
}
export function setup(ctx: SpindleFrontendContext) {
  // Read synchronously
  const state = ctx.ui.events.getKeyboardState()

  // Subscribe to changes (returns an unsubscribe function)
  const unsub = ctx.ui.events.onKeyboardChange((newState) => {
    if (newState.visible) {
      console.log(`Keyboard opened. Safe inset bottom: ${newState.insetBottom}px`)
    }
  })
}

Drawer State

Track when the side portrait drawer is opened, closed, or switched to a different tab.

export interface SpindleUIDrawerState {
  open: boolean;
  tabId: string | null;
}
export function setup(ctx: SpindleFrontendContext) {
  const unsub = ctx.ui.events.onDrawerChange(({ open, tabId }) => {
    if (open && tabId === 'my-extension-tab') {
      console.log('User opened my drawer tab!')
    }
  })
}

Settings State

Track when the settings modal is opened, closed, or switched to a different view.

export interface SpindleUISettingsState {
  open: boolean;
  view: string;
}
export function setup(ctx: SpindleFrontendContext) {
  const unsub = ctx.ui.events.onSettingsChange(({ open, view }) => {
    if (open && view === 'my-extension-settings') {
      console.log('User opened my extension settings panel!')
    }
  })
}

DOM Action Delegation

bindActionHandlers is a helper for binding generic interaction events (click, pointer down, etc.) onto extension-owned DOM, such as elements injected via ctx.dom.inject(), mounted via ctx.ui.mount(), or returned by ctx.ui.showModal().

Instead of adding dozens of individual .addEventListener() calls, you can define a dictionary of action handlers. The helper binds a single delegating event listener to the root container and maps events back to your callbacks.

Usage

export function setup(ctx: SpindleFrontendContext) {
  // 1. Create your extension-owned DOM
  const root = ctx.ui.mount('sidebar')
  root.innerHTML = `
    <div id="btn-approve" class="btn">Approve</div>
    <div id="btn-reject" class="btn">Reject</div>
  `

  // 2. Bind action handlers
  // The dictionary keys map to the "id" attribute of descendants by default.
  const unbind = ctx.ui.events.bindActionHandlers(root, {
    'btn-approve': (detail) => {
      console.log('Approved!', detail.element, detail.originalEvent)
    },
    'btn-reject': () => {
      console.log('Rejected!')
    }
  })
}

Advanced Binding

You can override the matching attribute and listen to different pointer events.

const root = ctx.dom.inject('body', `
  <button data-action="swipe-left">Left</button>
  <button data-action="swipe-right">Right</button>
`)

const unbind = ctx.ui.events.bindActionHandlers(root, {
  'swipe-left': (detail) => { /* ... */ },
  'swipe-right': (detail) => { /* ... */ }
}, {
  attribute: 'data-action',
  events: ['pointerdown', 'pointerup']
})

Note: The binding target may be the owned placement root or any owned descendant inside a registered placement root (for example, a mounted action container). The helper throws if you target system UI or another extension's DOM.

The target must be connected and live when binding. detail.root (the event.root value) is the exact target supplied to bindActionHandlers, while liveness, permission, and revocation follow its nearest registered owner root. Removing or revoking that owner root automatically unbinds the listener; the returned unbind function remains safe to call after automatic cleanup.