Skip to content

Conversation

@myers
Copy link
Collaborator

@myers myers commented Oct 28, 2025

Hello! 您好

This PR builds on the work in #35 (HeadlessCanvas). It depends on BridgeEventBinding from #32 for test infrastructure.

When scrolling through examples in storybook, it was annoying that when I scrolled while the pointer was over a canvas-ui element, it did nothing. This is the fix I came up with after digging into it.

This PR implements proper wheel event bubbling behavior in RenderScrollView to enable nested scrolling scenarios where parent scrollers can handle events when the inner scroller reaches its scroll bounds.

Changes:

  • RenderScrollView: only call preventDefault() when scroll actually occurs (when _setScrollOffset() returns true), allowing events to bubble to parent when at scroll bounds (top/bottom for vertical, left/right for horizontal)
  • DOMEventBinding: remove immediate preventDefault() on wheel events - let Canvas UI decide
  • SyntheticEventManager: conditionally call preventDefault() on native event only if a Canvas UI component handled it
  • Add 6 comprehensive integration tests covering preventDefault behavior at all scroll bounds and nested ScrollView scenarios

Tests verify:

  • preventDefault called when scrolling within bounds
  • preventDefault NOT called when at top/bottom/left/right bounds
  • Nested ScrollViews: inner at bounds allows outer to scroll

myers added 7 commits October 12, 2025 17:49
…actory

Separate frame scheduling concerns from canvas creation for better
architectural clarity and simpler custom integrations (e.g., WebXR).

Changes:
- Split IPlatformAdapter into two focused interfaces:
  * IFrameScheduler - Frame scheduling (scheduleFrame, onFrame)
  * ICanvasFactory - Canvas creation and management
- Update RenderCanvas to depend only on IFrameScheduler
  * Rename parameter from platformAdapter to frameScheduler
  * RenderCanvas no longer depends on canvas creation methods
- Update createElement factory to accept IFrameScheduler
- Keep IPlatformAdapter as union of both for backward compatibility

Benefits:
- Single Responsibility Principle - each interface has one concern
- RenderCanvas only depends on what it needs (frame scheduling)
- WebXR/custom renderers can provide simple IFrameScheduler without
  implementing unused canvas creation methods
- No breaking changes - IPlatformAdapter still works as before
- Better foundation for testing and exotic platforms

Example WebXR usage:
  class XRFrameScheduler implements IFrameScheduler {
    scheduleFrame() { xrSession.requestAnimationFrame(...) }
    onFrame(cb) { ... }
  }
  new RenderCanvas(new XRFrameScheduler(xrSession))
Fix pointerenter and pointerleave events to fire on entire element
hierarchy, not just the direct target.

Previously, these events only fired at the primary target element,
which was incorrect. They should fire for all elements that are
entered or left during pointer movement.

Now properly tracks which elements were entered (in new path but not
old) and which were left (in old path but not new), firing appropriate
events for each.
This commit adds support for injecting custom event bindings into RenderCanvas
and introduces BridgeEventBinding for programmatic event injection.

Changes:
- Add binding parameter to RenderCanvas constructor (defaults to DOMEventBinding)
- Add binding parameter to createElement factory function
- Implement BridgeEventBinding for programmatic event injection
- Add NativeEventBinding.el property for element binding
- Add comprehensive test suite for BridgeEventBinding (12 tests)

Benefits:
- Enables WebXR controller event injection
- Clean interface using property setter pattern
This commit enables passing any canvas element (HTMLCanvasElement or OffscreenCanvas)
to RenderCanvas via constructor injection, following the established dependency injection
pattern (frameScheduler, binding).

Changes:
- Add canvas parameter to RenderCanvas constructor and createElement factory
- Expand el property type to accept both HTMLCanvasElement and OffscreenCanvas
- Update CanvasSurface to accept both HTMLCanvasElement and OffscreenCanvas
- Rename createOrUpdateEl → ensureCanvasSize (reflects actual behavior)
- Guard cursor management with instanceof check for HTMLCanvasElement
- Make drawFrame public for manual rendering control
- Add JSDoc documentation for constructor and drawFrame

Benefits:
- Clean dependency injection pattern consistent with other constructor parameters
- Manual frame control via public drawFrame method
- Cursor management gracefully degrades for OffscreenCanvas
- Backwards compatible (canvas parameter is optional)
This commit adds HeadlessCanvas, a React component for rendering Canvas UI to
OffscreenCanvas without mounting to the DOM.

Changes:
- Add HeadlessCanvas component with event injection API
- Accept user-provided OffscreenCanvas and custom frameScheduler
- Provide injectEvent and injectWheelEvent helpers via onReady callback
- Support onFrameEnd callback for texture copying
- Export HeadlessCanvas and related types from @canvas-ui/react

Benefits:
- Enables WebXR layer rendering (XRWebGLLayer texture targets)
- Manual frame control with custom schedulers
- Programmatic event injection for XR controllers
- No DOM coupling for headless environments

Use cases:
- WebXR UI panels rendered to quad layers
- Custom render loop integration
getBoundingClientRect() only exists on HTMLCanvasElement, not OffscreenCanvas.
Skip popup positioning for OffscreenCanvas since it has no DOM position.
…crolling

Implement proper wheel event bubbling behavior in RenderScrollView to enable
nested scrolling scenarios where parent scrollers can handle events when the
inner scroller reaches its scroll bounds.

Changes:
- RenderScrollView: only call preventDefault() when scroll actually occurs
  (when _setScrollOffset() returns true), allowing events to bubble to parent
  when at scroll bounds (top/bottom for vertical, left/right for horizontal)
- Add 6 comprehensive integration tests covering preventDefault behavior at
  all scroll bounds and nested ScrollView scenarios
- BridgeEventBinding: add preventDefault/stopPropagation mocks to wheel events
  for test support, plus stub pointer capture methods for compatibility

Tests verify:
- preventDefault called when scrolling within bounds
- preventDefault NOT called when at top/bottom/left/right bounds
- Nested ScrollViews: inner at bounds allows outer to scroll

All tests passing (10 passed: 4 existing + 6 new)
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