refactor(scrollbar): align ScrollbarComponent with CameraComponent architecture#8693
Conversation
…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>
There was a problem hiding this comment.
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, andhandleEntitystorage intoScrollbarComponentinstance fields; reduceScrollbarComponentDatatoenabled. - Update
ScrollbarComponentSystemto use_schema = ['enabled'], explicit initialization property ordering, and an explicitcloneComponent. - Add a comprehensive
ScrollbarComponentunit 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.
| 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; | ||
| } |
There was a problem hiding this comment.
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.
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>
* 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>
…architecture (#8777) * refactor(scrollview): align ScrollViewComponent with CameraComponent architecture Apply the same refactor pattern recently adopted by ScrollbarComponent (#8693): the component now owns its state directly via private fields, setters do their work inline, the data-bag round-trip and set_<name> event chain are gone, and the system declares `_schema = ['enabled']` with an explicit `_properties` list and `cloneComponent`. `mouseWheelSensitivity` previously relied on the schema's `type: 'vec2'` auto-conversion (clones Vec2 inputs, builds a Vec2 from `[x, y]` arrays); that work is moved into the setter itself so JSON-loaded scenes shipping `mouseWheelSensitivity: [1, 1]` and callers passing shared Vec2 instances continue to work. Add `test/framework/components/scroll-view/component.test.mjs` covering defaults, the round-trip via `addComponent`, the Vec2/array normalization on `mouseWheelSensitivity`, side effects on `horizontal`/`vertical`, `set:scroll`, all four entity setters (Entity / GUID / null / unsubscribe-on-reassign), `cloneComponent`, and `resolveDuplicatedEntityReferenceProperties`. No public API change: properties, behavior, and the `set:scroll` event are preserved. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(scrollview): broaden mouseWheelSensitivity input + tear down refs on remove Two follow-ups from Copilot's review on #8777: - `mouseWheelSensitivity` setter was narrower than the old schema-driven `convertValue('vec2')`: it only normalized `Array.isArray(arg)`, so a Float32Array (or any other indexable input) would slip through to the passthrough branch and break later code that reads `.x`/`.y`. Match the old contract: nullish passes through, Vec2 is cloned, anything else is treated as indexable. - `onRemove` only detached the entity's own `element:add` and element listeners. Listeners registered on referenced viewport / content / scrollbar entities, plus the pending `_evtElementRemove` once-handle on the element, stayed live - keeping the removed component reachable from those entities. Set every entity ref to null (which routes through the existing setter -> unsubscribe path) and `.off()` the pending once-handle. This is also a pre-existing leak, not a regression from the refactor. Adds tests for both: a Float32Array round-trip on `mouseWheelSensitivity`, and a `removeComponent` test asserting that referenced entities no longer have any of the component's listeners. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(scrollview): skip explicit undefined in initializeComponentData Copilot review on #8777 caught a behavior regression: the old initializeComponentData normalized dragThreshold / useMouseWheel / mouseWheelSensitivity when they were `=== undefined`, so callers shipping `{ dragThreshold: undefined }` still ended up with the default 10. The refactored loop guarded with `hasOwnProperty`, which let explicit undefined values clobber the class-field defaults - turning `useMouseWheel` falsy (disables wheel scrolling) and `mouseWheelSensitivity` into undefined (`_onMouseWheel` then throws on `.x`/`.y`). Tighten the guard to `data[property] !== undefined` so the class-field defaults survive explicit-undefined input, exactly matching the legacy behavior. Adds a test that addComponent with `{ dragThreshold: undefined, useMouseWheel: undefined, mouseWheelSensitivity: undefined }` lands on the documented defaults. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
Refactor
ScrollbarComponentto mirrorCameraComponent/LightComponent's architecture, continuing the move away from*ComponentDataproperty bags.ScrollbarComponentnow owns_orientation,_value,_handleSize, and_handleEntitydirectly. Properties no longer round-trip throughdata[name]; the_setValuehelper, the internalset_*event chain, and the_onSet*/_toggleLifecycleListenersplumbing are all gone. The clamp-to-[0, 1]and 1e-5 epsilon semantics forvalueandhandleSizeare inlined into the setters.ScrollbarComponentDatais reduced to a singleenabledfield.ScrollbarComponentSystemmatchesCameraComponentSystem:_schema = ['enabled'], an explicit property list drivesinitializeComponentData, an explicitcloneComponentis added (the default base-class clone would otherwise lose every non-enabledfield), andComponent._buildAccessors(ScrollbarComponent.prototype, _schema)handles theenabledaccessor.Public API
No additions, removals, or behaviour changes. The full property surface of
ScrollbarComponentis preserved (verified against the regenerated.d.ts):enabled,orientation,value,handleSize,handleEntity, and the staticEVENT_SETVALUE/set:valueevent still consumed byScrollViewComponent.Tests
Adds
test/framework/components/scrollbar/component.test.mjs, modelled on theLightComponenttest suite, covering#addComponent(defaults + round-trip),#value(clamp +set:valuefiring + 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), andresolveDuplicatedEntityReferenceProperties. The suite was confirmed green againstmainbefore the refactor and remains green after it.Test plan
npm run lintpassesnpm run build:typesregenerates.d.tscleanly;npm run test:typespassesnpm test— 1688 passing, 0 failing