Skip to content

refactor(scrollbar): tighten ScrollbarComponent internals#8694

Merged
willeastcott merged 4 commits intomainfrom
refactor/scrollbar-tighten-internals
May 6, 2026
Merged

refactor(scrollbar): tighten ScrollbarComponent internals#8694
willeastcott merged 4 commits intomainfrom
refactor/scrollbar-tighten-internals

Conversation

@willeastcott
Copy link
Copy Markdown
Contributor

@willeastcott willeastcott commented May 6, 2026

Summary

Small follow-up cleanup to #8693, polishing the ScrollbarComponent internals while the file is fresh, plus two correctness fixes for ElementDragHelper lifecycle bugs that Copilot flagged on #8693 and on this PR's first commit.

Cleanup

  • Declare _handleDragHelper as a class field (@type {ElementDragHelper|null}, default null). It was previously implicit-via-assignment in _onHandleElementGain, the only piece of state on the component without a class-level declaration.
  • Tighten the handleEntity setter: resolve the candidate entity first, then a single early-return covers both "same Entity by ref" and "same GUID-string by ref" cases. Switch || null to ?? null.
  • Drop the _onSetHandleAlignment passthrough - bind _updateHandlePositionAndSize directly to the set:anchor / set:margin / set:pivot events on the handle element.
  • Use an early return in _updateHandlePositionAndSize, drop the handleElement local.
  • _destroyDragHelper now uses optional chaining and nulls the field after destroy.
  • Inline the _setHandleDraggingEnabled helper into onEnable / onDisable.

Bug fixes

  • A fresh ElementDragHelper built in _onHandleElementGain now mirrors the component's current enabled state, instead of inheriting the helper's default true. Previously, attaching a handle (or its element appearing) to an already-disabled scrollbar produced a draggable helper.
  • The orientation setter now rebuilds the drag helper (and refreshes the handle position / size) when an element is already attached. ElementDragHelper captures its axis at construction, so without this, flipping orientation at runtime left drags constrained to the old axis and could stop value updates from happening.

Both bug fixes have regression tests.

Public API

No additions, removals, or behaviour changes.

Test plan

  • npm run lint passes
  • npm run build:types regenerates .d.ts cleanly; npm run test:types passes
  • npm test - 1690 passing, 0 failing (18 ScrollbarComponent tests, including 2 new regression tests for the bug fixes above)

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>
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

Follow-up refactor of ScrollbarComponent internals to simplify handle wiring and improve drag-helper lifecycle handling after the larger architecture alignment work in #8693.

Changes:

  • Declares _handleDragHelper as an explicit private class field and improves _destroyDragHelper() cleanup.
  • Simplifies handleEntity setter by resolving the candidate entity up-front and using a single equality early-return.
  • Removes _onSetHandleAlignment passthrough and binds _updateHandlePositionAndSize directly to handle element alignment-related events; minor simplification inside _updateHandlePositionAndSize.

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

Comment thread src/framework/components/scrollbar/component.js
… 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>
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>
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

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

Comments suppressed due to low confidence (1)

src/framework/components/scrollbar/component.js:360

  • onRemove() only destroys the drag helper, but the component can also have active subscriptions on the current handle entity / handle element (_evtHandleEntityElementAdd and _evtHandleEntityChanges). If the scrollbar component is removed at runtime, these listeners will keep the component alive and can still invoke callbacks after removal. Consider calling _handleEntityUnsubscribe() (and optionally clearing _handleEntity) in onRemove() to fully detach.
    onRemove() {
        this._destroyDragHelper();
    }

Comment thread src/framework/components/scrollbar/component.js
…nent

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>
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

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

@willeastcott willeastcott merged commit 10e5cb4 into main May 6, 2026
12 checks passed
@willeastcott willeastcott deleted the refactor/scrollbar-tighten-internals branch May 6, 2026 20:46
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