Skip to content

feat(component): Adds SmartScrollbar to ui-next - OHIF-2558#5924

Merged
jbocce merged 21 commits into
OHIF:masterfrom
dan-rukas:feat/smart-scrollbar
Apr 6, 2026
Merged

feat(component): Adds SmartScrollbar to ui-next - OHIF-2558#5924
jbocce merged 21 commits into
OHIF:masterfrom
dan-rukas:feat/smart-scrollbar

Conversation

@dan-rukas
Copy link
Copy Markdown
Member

@dan-rukas dan-rukas commented Mar 26, 2026

Context

This PR adds a SmartScrollbar component to @ohif/ui-next that visualizes slice loading progress, viewed state, and scroll position in viewports.

The component is pure UI — it has no Cornerstone or viewer dependencies. It accepts a slice index, total count, loading state, and Uint8Arrays of marked positions (loaded/viewed), and handles all measurement, animation, pointer interaction, and visual state internally.

Preview the component, animations, and behaviors here: https://ohif-scroll-design-final.netlify.app/

SmartScrollbar-visual-states

Changes & Results

New files — 8 component files in platform/ui-next/src/components/SmartScrollbar/:

File Role
SmartScrollbar.tsx Root — dual context providers (layout + scroll), ResizeObserver, pointer handling, contraction state
SmartScrollbarTrack.tsx Dot-grid background, visible during loading
SmartScrollbarFill.tsx Pixel-aligned colored bars for contiguous marked runs (loaded and viewed)
SmartScrollbarIndicator.tsx Pill-shaped position marker, aligned to pixel buckets
SmartScrollbarEndpoints.tsx Endcap markers at marked range boundaries
useByteArray.ts Hook for managing a mutable Uint8Array with version-based change detection and optional debounce
utils.ts Pixel-fill mapping, contiguous run detection, indicator layout math
index.ts Barrel export

Modifiedplatform/ui-next/src/components/index.ts to export the new components.

Usage:

const loaded = useByteArray(imageIds.length);
const viewed = useByteArray(imageIds.length);

<SmartScrollbar value={index} total={295} onValueChange={setIndex} isLoading={loading}>
  <SmartScrollbarTrack>
    <SmartScrollbarFill marked={loaded.bytes} version={loaded.version} className="bg-neutral/25" loadingClassName="bg-neutral/50" />
    <SmartScrollbarFill marked={viewed.bytes} version={viewed.version} className="bg-primary/35" />
  </SmartScrollbarTrack>
  <SmartScrollbarIndicator />
  <SmartScrollbarEndpoints marked={loaded.bytes} version={loaded.version} />
</SmartScrollbar>

Key behaviors:

  • Track contracts from 8px to 4px after loading completes (600ms settle delay, 300ms ease transition)
  • Expands on hover or drag
  • Dot-grid pattern fades in during loading, fades out on completion
  • Loaded fill brightens during loading (50% opacity), dims after (25%)
  • Endpoint caps stay stationary during contraction (rendered via portal to a stable layer)
  • Click-to-jump and drag-to-scrub with pointer capture
  • Fills and endpoints render in pixel space — integer coordinates prevent sub-pixel bleeding and false gaps
  • Memoized subcomponents with version-based change detection for efficient re-renders
  • Error thrown if SmartScrollbarIndicator is omitted
  • Uses existing OHIF design tokens (--neutral, --primary, --foreground)
  • Fill colors are consumer-configurable via Tailwind class props

Testing

No Testing Needed.

This PR adds a UI component only — no viewer integration is included. The component can be previewed and tested interactively here:

Live demo: https://ohif-scroll-design-final.netlify.app/

  • Press Play to see the loading lifecycle (dot-grid, fills, contraction)
  • Scroll the viewport to move the indicator
  • Hover the contracted scrollbar to see it expand
  • Drag the indicator to scrub through slices

Checklist

PR

  • My Pull Request title is descriptive, accurate and follows the
    semantic-release format and guidelines.

Code

  • My code has been well-documented (function documentation, inline comments,
    etc.)

Public Documentation Updates

  • The documentation page has been updated as necessary for any public API
    additions or removals.

Tested Environment

  • OS: macOS Sequoia 15.7.4 (24G517), Apple M4 Pro
  • Node version: 20.19.1
  • Browser: Google Chrome (latest) 146.0.7680.165 (Official Build) (arm64)

Greptile Summary

This PR adds a well-designed SmartScrollbar compound component to @ohif/ui-next that visualizes slice loading progress, viewed state, and scroll position. The implementation is pure UI with no Cornerstone dependencies, uses context providers for layout and scroll state, handles pointer interaction with capture, and renders fills/endpoints in pixel space to prevent sub-pixel bleeding.

Previous review concerns (stale fills with in-place mutation, ref timing for portal layer, keyboard accessibility conflicts, unused svgIdPrefix, fill bar overflow) have all been addressed. Two minor issues remain:

  • Missing useMemo in SmartScrollbarEndpoints: computePixelFilledFromMarked and the first/last pixel scan run synchronously on every context change (including hover/drag which update effectiveWidth). Since SmartScrollbarLayoutContext is recreated whenever effectiveWidth changes, the endpoint component re-renders on every hover event and repeats an O(n) pass over the pixel array. For large volumes (1000+ slices), this is an unnecessary allocation and scan on every pointer-enter/leave. Wrapping with useMemo([marked, version, trackHeight, fillPadding]) would avoid this.
  • Shallow child validation: validateChildren only scans top-level children via Children.forEach. If a consumer wraps SmartScrollbarIndicator in a React.Fragment or conditional render (e.g., {condition && <SmartScrollbarIndicator />}), the check fails to detect it and throws incorrectly. This is a potential footgun for consumers.

Confidence Score: 5/5

Safe to merge — all previous P1 concerns have been resolved; remaining findings are P2 style/performance suggestions.

All blocking issues from previous review rounds (ref timing, stale fills, keyboard conflicts, unused context fields, run length overflow) have been addressed. The two remaining findings are P2: a missing useMemo in SmartScrollbarEndpoints (performance concern for large volumes) and a shallow child scan that can throw for wrapped indicators. Neither causes incorrect behavior in the documented usage pattern.

SmartScrollbarEndpoints.tsx (missing memoization for pixel computation) and SmartScrollbar.tsx (shallow validateChildren scan).

Important Files Changed

Filename Overview
platform/ui-next/src/components/SmartScrollbar/SmartScrollbar.tsx Root component with dual context providers, ResizeObserver, opt-in keyboard nav, pointer handling, and contraction state — all previous review concerns addressed.
platform/ui-next/src/components/SmartScrollbar/SmartScrollbarEndpoints.tsx Portal-based endpoint cap renderer; missing useMemo for O(n) pixel computation that re-runs on every context change (hover/drag).
platform/ui-next/src/components/SmartScrollbar/SmartScrollbarFill.tsx Pixel-aligned fill bars with correct useMemo and version-based invalidation; clean implementation.
platform/ui-next/src/components/SmartScrollbar/SmartScrollbarIndicator.tsx Pill-shaped position indicator aligned to pixel buckets; straightforward and correct.
platform/ui-next/src/components/SmartScrollbar/SmartScrollbarTrack.tsx Dot-grid background with fade animation; correctly uses useId for pattern IDs and memo for re-render control.
platform/ui-next/src/components/SmartScrollbar/utils.ts Pure utility functions for pixel mapping, run detection, and indicator layout; well-tested with good edge-case coverage.
platform/ui-next/src/components/SmartScrollbar/utils.test.ts Solid unit test coverage for computePixelFilledFromMarked and computeContiguousRuns including boundary conditions.
platform/ui-next/src/components/SmartScrollbar/useByteArray.ts Mutable Uint8Array with version-based React invalidation, optional debounce, and well-documented API contract.
platform/ui-next/src/components/SmartScrollbar/index.ts Barrel export for all SmartScrollbar components and hooks.
platform/ui-next/src/components/index.ts Adds SmartScrollbar exports to the ui-next component index.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: platform/ui-next/src/components/SmartScrollbar/SmartScrollbarEndpoints.tsx
Line: 35-54

Comment:
**Missing `useMemo` for O(n) pixel computation**

`computePixelFilledFromMarked` and the first/last pixel scan run unconditionally on every render. `SmartScrollbarEndpoints` consumes `SmartScrollbarLayoutContext`, which is recreated whenever `effectiveWidth` changes — meaning this component re-renders (and repeats the full O(n) scan) on every hover and drag event, even though `trackHeight` and `fillPadding` (the actual inputs to the computation) haven't changed.

For a typical OHIF volume with 1000–3000 slices, that's an unnecessary allocation and scan on every pointer-enter/leave. `SmartScrollbarFill` already wraps its equivalent computation in `useMemo` — the same pattern should be applied here:

```tsx
const { firstFilledPixel, lastFilledPixel } = useMemo(() => {
  const pixelCount = Math.max(0, Math.floor(trackHeight - fillPadding * 2));
  if (pixelCount === 0) return { firstFilledPixel: -1, lastFilledPixel: -1 };
  const pixelFilled = computePixelFilledFromMarked(marked, pixelCount);
  let first = -1;
  let last = -1;
  for (let pixel = 0; pixel < pixelFilled.length; pixel++) {
    if (pixelFilled[pixel]) { first = pixel; break; }
  }
  for (let pixel = pixelFilled.length - 1; pixel >= 0; pixel--) {
    if (pixelFilled[pixel]) { last = pixel; break; }
  }
  return { firstFilledPixel: first, lastFilledPixel: last };
}, [marked, version, trackHeight, fillPadding]);
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: platform/ui-next/src/components/SmartScrollbar/SmartScrollbar.tsx
Line: 16-30

Comment:
**Shallow child scan misses wrapped indicators**

`validateChildren` uses `Children.forEach` which only iterates the *direct* top-level children. If a consumer wraps `SmartScrollbarIndicator` in a `React.Fragment` or a conditional expression, the check won't find it and will throw, even though the indicator is present:

```tsx
// Both of these incorrectly throw:
<SmartScrollbar ...>
  <>
    <SmartScrollbarTrack />
    <SmartScrollbarIndicator />
  </>
</SmartScrollbar>

<SmartScrollbar ...>
  <SmartScrollbarTrack />
  {showIndicator && <SmartScrollbarIndicator />}
</SmartScrollbar>
```

The second case is especially problematic — during an initial render where `showIndicator` is `false` (e.g., waiting for data), the component crashes the React tree even though the consumer intends to add the indicator once data arrives.

Consider documenting that the indicator must be an unconditional, unwrapped direct child, or use a recursive `Children.forEach` / context-flag approach to detect it at any nesting depth.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (5): Last reviewed commit: "Merge remote-tracking branch 'origin/mas..." | Re-trigger Greptile

@netlify
Copy link
Copy Markdown

netlify Bot commented Mar 26, 2026

Deploy Preview for ohif-dev ready!

Name Link
🔨 Latest commit c6691e7
🔍 Latest deploy log https://app.netlify.com/projects/ohif-dev/deploys/69d3a641c5d39500086b518f
😎 Deploy Preview https://deploy-preview-5924--ohif-dev.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@dan-rukas dan-rukas requested a review from jbocce March 26, 2026 19:42
Comment thread platform/ui-next/src/components/SmartScrollbar/SmartScrollbarEndpoints.tsx Outdated
Comment thread platform/ui-next/src/components/SmartScrollbar/SmartScrollbarFill.tsx Outdated
Comment thread platform/ui-next/src/components/SmartScrollbar/SmartScrollbar.tsx Outdated
Comment thread platform/ui-next/src/components/SmartScrollbar/SmartScrollbar.tsx Outdated
Comment thread platform/ui-next/src/components/SmartScrollbar/utils.ts Outdated
Comment thread platform/ui-next/src/components/SmartScrollbar/SmartScrollbar.tsx Outdated
@jbocce jbocce changed the title feat(component): Adds SmartScrollbar to ui-next feat(component): Adds SmartScrollbar to ui-next - OHIF-2558 Mar 27, 2026
Comment thread platform/ui-next/src/components/SmartScrollbar/SmartScrollbar.tsx Outdated
Comment thread platform/ui-next/src/components/SmartScrollbar/SmartScrollbar.tsx Outdated
… unnecessary re-renders.

Memoize SmartScrollbar components to prevent unnecessary re-renders.
@jbocce
Copy link
Copy Markdown
Collaborator

jbocce commented Mar 31, 2026

The SmartScrollbar component is supposed to be somewhat generic. Eliminating references to slices would be prudent. I will push a commit to address this soon.

@jbocce
Copy link
Copy Markdown
Collaborator

jbocce commented Apr 1, 2026

The SmartScrollbar component is supposed to be somewhat generic. Eliminating references to slices would be prudent. I will push a commit to address this soon.

Commit hash a388084 addressed this.

@jbocce
Copy link
Copy Markdown
Collaborator

jbocce commented Apr 1, 2026

@greptileai I just pushed bc80fae that addresses a concern we had for ensuring the fills accurately indicate marked items without over and/or under stating items that are marked. This is especially important considering that one fill is used to indicate viewed (and unviewed) medical images. Please have a look and let me know what you think. Thanks.

Comment thread platform/ui-next/src/components/SmartScrollbar/SmartScrollbar.tsx
@jbocce
Copy link
Copy Markdown
Collaborator

jbocce commented Apr 1, 2026

@greptileai I just pushed some unit tests for some of the SmartSrollbar utilities. Please have a look and let me know if there are any you think I should add. Thanks.

@dan-rukas
Copy link
Copy Markdown
Member Author

Tested the latest code and changes and everything looks great and matches the original design and behaviors. Thanks again @jbocce for your help, changes, and detailed approach to getting this right.

@jbocce jbocce merged commit 91a8715 into OHIF:master Apr 6, 2026
8 of 9 checks passed
wayfarer3130 pushed a commit that referenced this pull request Apr 10, 2026
Co-authored-by: Joe Boccanfuso <joe.boccanfuso@radicalimaging.com>
wayfarer3130 pushed a commit that referenced this pull request Apr 10, 2026
Co-authored-by: Joe Boccanfuso <joe.boccanfuso@radicalimaging.com>
wayfarer3130 added a commit that referenced this pull request Apr 15, 2026
* fix: Use newer ONNX version and load without errors

* Only changes to enable SAM again

* fix(seg hydration): auto-hydrate RT struct on second load with disableConfirmationPrompts (#5875)

* chore(version): Update package versions to 3.13.0-beta.34 [skip ci]

* fix(Threshold tool): Threshold tool no longer becomes deselected when the Dynamic option is selected (#5884)

fix(Threshold tool): Added 'ThresholdCircularBrushDynamic' to the toolNames array so the evaluator correctly recognizes it as an active state for the Threshold button when Dynamic mode is selected.

* chore(version): Update package versions to 3.13.0-beta.35 [skip ci]

* fix: Modalities in study list should select starts with as primary (#5886)

* chore(version): Update package versions to 3.13.0-beta.36 [skip ci]

* fix(security): Bump tar version to address CVE-2026-31802. (#5893)

* chore(version): Update package versions to 3.13.0-beta.37 [skip ci]

* fix(segmentation): Display "No description S:{series number} {modality}" for segmentations with no label. (#5874)

* Bump CS3D dependency to get the fallbackLabel field additions.

* chore(version): Update package versions to 3.13.0-beta.38 [skip ci]

* fix(window level): The window level value is not displayed by default on all the viewports when selecting common/custom layout and TMTV. (#5865)

* fix(window level): Set up listener for viewport availability such that the initial window level can be read and displayed.

* PR feedback.

* PR feedback.

---------

Co-authored-by: Bill Wallace <wayfarer3130@gmail.com>

* chore(version): Update package versions to 3.13.0-beta.39 [skip ci]

* fix(security): Bump flattened version to address CVE-2026-32141. (#5897)

* chore(version): Update package versions to 3.13.0-beta.40 [skip ci]

* fix(sr-hydration): enable hydration and arrow navigation for 3D SR measurements (#5887)

Joe is away, so approving based on the code having the requested change, and otherwise looking good/passing tests.

* fix(sr-hydration): enable hydration and arrows navigation for 3D SR measurements

* test: add automated test for SR measurement navigation with arrows after hydration

* add cross-study warning in the 3D branch

* test: address reviewer feedback for the test

* fix: support 3D and 2D annotations for SR hydration

* test: improve navigation to first image

---------

Co-authored-by: Bill Wallace <wayfarer3130@gmail.com>

* chore(version): Update package versions to 3.13.0-beta.41 [skip ci]

* feat: Add combined build (#5895)

* Add combined build

* Link script location update

* Security and validation fixes

* Allow specifying target path in PR description

* fix: Version match

* Fix build detection issue

* fix: Playwright deploy

* Separate out the branch merge guard

* Update docs and link info

* test: Update the layout change to wait for network idle

* Move audit late so the rest of the build can be worked on

* Add text with network check to ensure we see this change is updated

* Attempt to fix the mpr loading on ohif-downstream

* PR review comments

* Update docs

* Update to CS3D 4.20.0

* PR comments

* Add log on ohif-integration builds

* Update build test

* Removed unused space to kickoff build

* chore(version): Update package versions to 3.13.0-beta.42 [skip ci]

* fix(SR): Added support for spline and live wire SR items. (#5870)

* fix(SR): Added support for spline and live wire SR items.

* Apply suggestion from @greptile-apps[bot]

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* Add a script to checkout a worktree for test builds

* fix: Allow download for testing sr validator

* Remove script that wasn't intended to be included

* Bump CS3D version.

* PR comments - simplify code and use single codepath for download

* Allow both download and save buttons for SEG and RTSTRUCT

---------

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: Bill Wallace <wayfarer3130@gmail.com>

* chore(version): Update package versions to 3.13.0-beta.43 [skip ci]

* chore(tests): contour segment interactions e2e tests - rename and togglevisibility (#5891)

* chore(version): Update package versions to 3.13.0-beta.44 [skip ci]

* chore(refactor): use public appConfig getter instead of private _appConfig field (#5923)

* chore(version): Update package versions to 3.13.0-beta.45 [skip ci]

* refactor(tests): update viewport page object usage to async and update all effected tests (#5927)

* chore(version): Update package versions to 3.13.0-beta.46 [skip ci]

* fix: prevent black viewport when navigating series with client-created segmentation (#5919)

* chore(version): Update package versions to 3.13.0-beta.47 [skip ci]

* fix(measurement): Restore viewport interactivity when deleting in-progress Spline or Livewire measurement (#5905)

* chore(version): Update package versions to 3.13.0-beta.48 [skip ci]

* fix(segmentation): restrict overlay segmentation menu to same frame of reference as viewport background display set  (#5900)

- Add FrameOfReferenceUID to SEG and RTSTRUCT displaySet in SOP Class Handlers so the FOR is available for filtering
- Sync optimisticOverlayDisplaySets when background display set changes so the overlay menu reflects the correct state after a background switch
- Add FOR matching guard to the hydrate segmentation synchronizer to prevent the hydration synchronizer from blindly mirroring segmentations from a source viewport to a target viewport if their primary Frames of Reference do not align.
- fix segmentation overlay order reversal on viewport re-render

* chore(version): Update package versions to 3.13.0-beta.49 [skip ci]

* fix(security): update dependencies to fix security vulnerabilities (#5936)

* chore(version): Update package versions to 3.13.0-beta.50 [skip ci]

* fix(security): Update yarn.lock that was missed in PR #5936. (#5940)

* chore(version): Update package versions to 3.13.0-beta.51 [skip ci]

* feat(component): Adds SmartScrollbar to ui-next - OHIF-2558 (#5924)

Co-authored-by: Joe Boccanfuso <joe.boccanfuso@radicalimaging.com>

* fix(defaultRouteInit): pass sorted display sets to hanging protocol for deterministic viewport order (#5933)

fix: pass sorted display sets to hanging protocol for deterministic viewport order

The `applyHangingProtocol` function already sorts display sets by modality
priority and series number into `sortedDisplaySets`, but the unsorted
`displaySets` array was being passed to `hangingProtocolService.run()`.

This caused non-deterministic viewport ordering across page loads because
`displaySetService.getActiveDisplaySets()` returns display sets in creation
order, which depends on asynchronous network responses.

Made-with: Cursor

* chore(version): Update package versions to 3.13.0-beta.52 [skip ci]

* revert: rename DisplaySet.frameOfReferenceUID back to FrameOfReferenceUID (#5943)

* chore(version): Update package versions to 3.13.0-beta.53 [skip ci]

* fix(cornerstone): read FrameOfReferenceUID from display set in viewport service (#5950)

* chore(version): Update package versions to 3.13.0-beta.54 [skip ci]

* fix: ignore auth in git (#5955)

* chore(version): Update package versions to 3.13.0-beta.55 [skip ci]

* ONNX latest version

* chore(version): Update package versions to 3.13.0-beta.56 [skip ci]

* bun lock

* fix high sev mathjs issue

* Revert onnx changes

* Update to recent CS3D version

* Undo unneeded change

* Add null check

* Undo unneeded change

---------

Co-authored-by: Ghadeer Albattarni <165973963+GhadeerAlbattarni@users.noreply.github.com>
Co-authored-by: ohif-bot <danny.ri.brown+ohif-bot@gmail.com>
Co-authored-by: Joe Boccanfuso <109477394+jbocce@users.noreply.github.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: diattamo <mmddiatta@gmail.com>
Co-authored-by: Pedro Köhler <pedrokohlerbh@gmail.com>
Co-authored-by: Dan Rukas <dan.rukas@gmail.com>
Co-authored-by: Joe Boccanfuso <joe.boccanfuso@radicalimaging.com>
Co-authored-by: Alireza <ar.sedghi@gmail.com>
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.

2 participants