Skip to content

refactor(scrollbar): align ScrollbarComponent with CameraComponent architecture#8693

Merged
willeastcott merged 2 commits intomainfrom
refactor/scrollbar-component-camera-style
May 6, 2026
Merged

refactor(scrollbar): align ScrollbarComponent with CameraComponent architecture#8693
willeastcott merged 2 commits intomainfrom
refactor/scrollbar-component-camera-style

Conversation

@willeastcott
Copy link
Copy Markdown
Contributor

Summary

Refactor ScrollbarComponent to mirror CameraComponent / LightComponent's architecture, continuing the move away from *ComponentData property bags.

  • ScrollbarComponent now owns _orientation, _value, _handleSize, and _handleEntity directly. Properties no longer round-trip through data[name]; the _setValue helper, the internal set_* event chain, and the _onSet* / _toggleLifecycleListeners plumbing are all gone. The clamp-to-[0, 1] and 1e-5 epsilon semantics for value and handleSize are inlined into the setters.
  • ScrollbarComponentData is reduced to a single enabled field.
  • ScrollbarComponentSystem matches CameraComponentSystem: _schema = ['enabled'], an explicit property list drives initializeComponentData, an explicit cloneComponent is added (the default base-class clone would otherwise lose every non-enabled field), and Component._buildAccessors(ScrollbarComponent.prototype, _schema) handles the enabled accessor.

Public API

No additions, removals, or behaviour changes. The full property surface of ScrollbarComponent is preserved (verified against the regenerated .d.ts): enabled, orientation, value, handleSize, handleEntity, and the static EVENT_SETVALUE / set:value event still consumed by ScrollViewComponent.

Tests

Adds test/framework/components/scrollbar/component.test.mjs, modelled on the LightComponent test suite, covering #addComponent (defaults + round-trip), #value (clamp + set:value firing + epsilon no-op), #handleSize (clamp + epsilon no-op), #orientation (opposite-dimension clear on change), #handleEntity (Entity / GUID-string / null / unsubscribe-on-reassign), #cloneComponent (scalar round-trip + handle remap), and resolveDuplicatedEntityReferenceProperties. The suite was confirmed green against main before the refactor and remains green after it.

Test plan

  • npm run lint passes
  • npm run build:types regenerates .d.ts cleanly; npm run test:types passes
  • npm test — 1688 passing, 0 failing

…chitecture

ScrollbarComponent now owns its `orientation`, `value`, `handleSize`,
and `handleEntity` state in private fields, mirroring the
CameraComponent / LightComponent pattern. The bespoke `_setValue` /
`data[name]` indirection and the internal `set_*` event chain are
removed, ScrollbarComponentData is reduced to `enabled`, and the
ScrollbarComponentSystem is now pure boilerplate driven by
`_schema = ['enabled']`, an explicit `_properties` list, and an
explicit `cloneComponent`. Adds a unit-test suite to guard the
behaviour through the migration.

Co-authored-by: Cursor <cursoragent@cursor.com>
@willeastcott willeastcott merged commit 5fadb06 into main May 6, 2026
8 checks passed
@willeastcott willeastcott deleted the refactor/scrollbar-component-camera-style branch May 6, 2026 19:36
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Refactors ScrollbarComponent to follow the newer component architecture used by CameraComponent / LightComponent, reducing reliance on *ComponentData property bags while preserving the public API and adding targeted unit tests.

Changes:

  • Move orientation, value, handleSize, and handleEntity storage into ScrollbarComponent instance fields; reduce ScrollbarComponentData to enabled.
  • Update ScrollbarComponentSystem to use _schema = ['enabled'], explicit initialization property ordering, and an explicit cloneComponent.
  • Add a comprehensive ScrollbarComponent unit test suite covering defaults, clamping/epsilon behavior, entity reference handling, cloning, and duplicated-entity remapping.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
test/framework/components/scrollbar/component.test.mjs Adds new unit coverage for the refactored ScrollbarComponent behaviors.
src/framework/components/scrollbar/system.js Aligns system initialization/cloning/accessors with schema-only enabled plus explicit properties list.
src/framework/components/scrollbar/data.js Reduces component data to enabled only.
src/framework/components/scrollbar/component.js Moves scrollbar state off the data bag into component-owned fields and simplifies setter/event plumbing.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 108 to +117
set orientation(arg) {
this._setValue('orientation', arg);
if (this._orientation === arg) {
return;
}

this._orientation = arg;

if (this._handleEntity?.element) {
this._handleEntity.element[this._getOppositeDimension()] = 0;
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed - this is a real (pre-existing) bug. ElementDragHelper captures its axis at construction, so flipping orientation at runtime leaves drags constrained to the old axis and can stop value updates from happening. Fixed in 6016d61bf as part of #8694: the orientation setter now rebuilds the drag helper and calls _updateHandlePositionAndSize() when a handle element is already attached. Regression test included.

willeastcott added a commit that referenced this pull request May 6, 2026
ElementDragHelper captures its constraint axis at construction time, so
flipping the scrollbar orientation while a handle element exists left
the helper still constraining drags to the old axis. _onHandleDrag
would then read the wrong component of the position vector and value
updates could stop working until the helper was destroyed and rebuilt
through some other path (e.g. handleEntity reassignment).

Extract the helper-rebuild logic from _onHandleElementGain into a
shared _rebuildDragHelper method, and call it (plus
_updateHandlePositionAndSize) from the orientation setter when there
is an active handle element.

Adds a regression test that flips the orientation post-attach and
verifies the drag helper is rebuilt for the new axis.

Reported by Copilot in #8693.

Co-authored-by: Cursor <cursoragent@cursor.com>
willeastcott added a commit that referenced this pull request May 6, 2026
* refactor(scrollbar): tighten ScrollbarComponent internals

Small follow-up cleanup to #8693:

- Declare _handleDragHelper as a class field (was implicit-via-assignment).
- Tighten the handleEntity setter: resolve the candidate entity first,
  then a single early-return covers both "same Entity by ref" and
  "same GUID by string" cases. Switch || null to ?? null.
- Drop the _onSetHandleAlignment passthrough; bind
  _updateHandlePositionAndSize directly to set:anchor / set:margin /
  set:pivot events on the handle element.
- Use an early return in _updateHandlePositionAndSize.
- _destroyDragHelper now uses optional chaining and nulls the field
  after destroy, so a post-element-loss enabled toggle no longer
  writes to a torn-down helper.
- Inline the _setHandleDraggingEnabled helper into onEnable / onDisable.

No public API changes. The 16 existing scrollbar tests still pass.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(scrollbar): respect component enabled state when building a fresh drag helper

ElementDragHelper defaults to enabled = true in its constructor, so a
helper created in _onHandleElementGain (e.g. when an element is added
to the handle entity after the scrollbar is already disabled) would
start out draggable even though onDisable had already run. Mirror the
component's current enabled state on the new helper to keep the
lifecycle consistent.

Adds a regression test that exercises the late-arriving-element path
on a disabled scrollbar.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(scrollbar): rebuild drag helper when orientation changes at runtime

ElementDragHelper captures its constraint axis at construction time, so
flipping the scrollbar orientation while a handle element exists left
the helper still constraining drags to the old axis. _onHandleDrag
would then read the wrong component of the position vector and value
updates could stop working until the helper was destroyed and rebuilt
through some other path (e.g. handleEntity reassignment).

Extract the helper-rebuild logic from _onHandleElementGain into a
shared _rebuildDragHelper method, and call it (plus
_updateHandlePositionAndSize) from the orientation setter when there
is an active handle element.

Adds a regression test that flips the orientation post-attach and
verifies the drag helper is rebuilt for the new axis.

Reported by Copilot in #8693.

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs(scrollbar): correct handleEntity JSDoc to reference ElementComponent

The handleEntity setter's JSDoc claimed the handle "must have a
ScrollbarComponent", but the code relies on the handle's element
(ElementDragHelper is constructed from handleEntity.element). Update
the doc to require an ElementComponent, with useInput: true for the
handle to be draggable. Reported by Copilot in #8694.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: ui UI related issue enhancement Request for a new feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants