Skip to content

feat(content-sidebar): add drag-to-resize handle behind feature flag#4559

Open
jmcbgaston wants to merge 5 commits into
masterfrom
preview-sidebar-resizable
Open

feat(content-sidebar): add drag-to-resize handle behind feature flag#4559
jmcbgaston wants to merge 5 commits into
masterfrom
preview-sidebar-resizable

Conversation

@jmcbgaston
Copy link
Copy Markdown
Contributor

@jmcbgaston jmcbgaston commented May 14, 2026

Description

Adds a pointer-driven drag-to-resize handle on the left edge of the ContentSidebar. The current default width becomes the minimum; users can grow the sidebar up to 50% of the viewport. Width is session-only (no persistence) — resets on reload.

The new SidebarResizeHandle is a mouse-only widget marked aria-hidden="true" (keyboard support deferred to a follow-up). It uses pointer capture during drag.

Gating:

  • Behind isFeatureEnabled(features, 'contentSidebar.resizable.enabled') — flag-off is a no-op
  • Additionally restricted to large / xlarge viewports via the existing withMediaQuery HOC. Small / medium viewports keep the original bottom-sheet layout.
  • SCSS overrides for .bcs-content and .bcs-activity-feed are scoped under .bcs-is-resizable so flag-off behavior is unchanged.

Reflow of the preview viewer happens automatically via the existing react-measurepreview.resize() chain — no new wiring needed.

Test plan

  • With features.contentSidebar.resizable.enabled = true, the handle renders on the left edge of the sidebar
  • Pointer drag grows the sidebar up to 50% of viewport; minimum is the un-resized default (400px / 440px AI variant)
  • Resizing the preview content shrinks/grows in response (viewer reflows)
  • With features.contentSidebar.resizable.enabled = false, no handle renders and sidebar widths are unchanged
  • On ≤ 767px viewport, no handle renders and sidebar collapses to bottom-sheet layout

Semantic release type

  • feat - New feature

Summary by CodeRabbit

  • New Features

    • Content sidebar is now resizable on larger screens; users can drag the sidebar edge to dynamically adjust its width.
  • Style

    • Updated sidebar and activity feed styling to properly support the new resizing functionality across different screen sizes.

Review Change Stack

Adds a pointer- and keyboard-accessible resize handle on the left edge
of the sidebar. Current default width becomes the minimum; maximum is
clamped at 50% of the viewport. Width state lives on the `Sidebar`
component and is session-only (no persistence).

The new `SidebarResizeHandle` component is a `role="separator"` with
live `aria-valuenow` / `aria-valuemin` / `aria-valuemax`, supports
ArrowLeft / ArrowRight / Home / End for keyboard resize, and uses
pointer capture during drag.

Gated by `isFeatureEnabled(features, 'contentSidebar.resizable.enabled')`
and additionally restricted to large / x-large viewports via the
existing `withMediaQuery` HOC — small / medium viewports keep the
original bottom-sheet layout. SCSS overrides for `.bcs-content` and
`.bcs-activity-feed` are scoped under `.bcs-is-resizable` so flag-off
behavior is unchanged.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 14, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c14d3795-4aa4-4ded-8082-f6222bbc73b1

📥 Commits

Reviewing files that changed from the base of the PR and between 94a887a and 6dbd5aa.

📒 Files selected for processing (1)
  • src/elements/content-sidebar/SidebarResizeHandle.js

Walkthrough

Adds a pointer-driven SidebarResizeHandle, wires resizable behavior behind a feature flag and viewport check in Sidebar, stores and clamps user width in state, applies inline width/maxWidth after user resize, and updates SCSS so sidebar content and activity feed fill the resized container.

Changes

Resizable Sidebar Feature

Layer / File(s) Summary
SidebarResizeHandle implementation
src/elements/content-sidebar/SidebarResizeHandle.js
Pointer-based resize grip with Flow props, clamp helper, pointerdown/move/up lifecycle, pointer capture where supported, global listener cleanup, dragging CSS state, and onResize callback.
Resize handle styling
src/elements/content-sidebar/SidebarResizeHandle.scss
Adds .bcs-resize-handle: 6px vertical handle, col-resize cursor, background transition, hover and dragging visual states.
Sidebar wiring and sizing logic
src/elements/content-sidebar/Sidebar.js
Imports withMediaQuery, extends Props/State and width constants, stores width in state via handleResize, gates resizable by feature flag and viewport size, computes/clamps minWidth/maxWidth/currentWidth, applies inline styles after user resize, toggles bcs-is-resizable class, and conditionally renders SidebarResizeHandle.
Responsive & content styling
src/elements/content-sidebar/ContentSidebar.scss, src/elements/content-sidebar/SidebarContent.scss, src/elements/content-sidebar/activity-feed/activity-feed/ActivityFeed.scss
Sets positioning context, scopes .bcs-is-resizable.bcs-is-open min-width overrides (with small-screen neutralization), forces .bcs-content/.bcs-scroll-content and activity feed to width: 100% when resizable, and removes prior .bcs-is-wider activity-feed width rule.
Sidebar tests (unit & integration)
src/elements/content-sidebar/__tests__/SidebarResizeHandle.test.js, src/elements/content-sidebar/__tests__/Sidebar.test.js
Unit tests simulate window pointer events to assert computed widths (expand/shrink), clamping to min/max, dragging class toggling, ARIA rendering, and removal of global listeners on unmount; integration tests validate feature-flag + viewport gating, bcs-is-resizable and handle presence, and inline aside width/maxWidth behavior (maxWidth capped to 50% of viewWidth).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested labels

ready-to-merge

Suggested reviewers

  • tjiang-box
  • jpan-box
  • Lindar90

Poem

🐰 I nudged the handle with a curious paw,
The sidebar stretched wide — what a sight to saw!
Drag left, drag right, the clamp keeps things neat,
A comfy container, snug and sweet.
Hops of joy for layouts kept discreet.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main feature being added: a drag-to-resize handle for the content sidebar that is gated behind a feature flag.
Description check ✅ Passed The PR description provides a comprehensive overview including functionality details, gating conditions, test plan, and semantic release type, covering all essential aspects of the change.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch preview-sidebar-resizable

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@jmcbgaston jmcbgaston marked this pull request as ready for review May 14, 2026 21:52
@jmcbgaston jmcbgaston requested review from a team as code owners May 14, 2026 21:52
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/elements/content-sidebar/SidebarResizeHandle.js`:
- Around line 39-64: The Flow errors come from TypeScript event types and
optional-call syntax; update handlePointerUp, handlePointerDown and
handleKeyDown to use Flow's SyntheticPointerEvent and SyntheticKeyboardEvent
types (instead of React.PointerEvent/React.KeyboardEvent) and replace every
optional-call like target.hasPointerCapture?.(...) and
event.currentTarget.setPointerCapture?.(...) and onResizeStart?.() /
onResizeEnd?.() with explicit null/undefined checks (e.g., if (target && typeof
target.hasPointerCapture === 'function')
target.hasPointerCapture(event.pointerId); and if (onResizeStart)
onResizeStart();). Ensure startXRef/current, startWidthRef/current,
setIsDragging, handlePointerMove remain unchanged but used with the new
Flow-compatible event types.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 02b0b1ec-8256-4d64-b578-9830c44e20d1

📥 Commits

Reviewing files that changed from the base of the PR and between 248012e and c89d504.

📒 Files selected for processing (6)
  • src/elements/content-sidebar/ContentSidebar.scss
  • src/elements/content-sidebar/Sidebar.js
  • src/elements/content-sidebar/SidebarContent.scss
  • src/elements/content-sidebar/SidebarResizeHandle.js
  • src/elements/content-sidebar/SidebarResizeHandle.scss
  • src/elements/content-sidebar/activity-feed/activity-feed/ActivityFeed.scss

Comment thread src/elements/content-sidebar/SidebarResizeHandle.js Outdated
- Make `maxWidth` reactive by reading `viewWidth` from withMediaQuery
  (previously read `window.innerWidth` at render time, leaving the cap
  stale across browser resizes)
- Drop unused `onResizeStart` / `onResizeEnd` props from
  `SidebarResizeHandle` — no caller passes them
- Simplify SCSS selectors: `.bcs-is-resizable.bcs-is-wider .bcs-content`
  was redundant with `.bcs-is-resizable .bcs-content` (same specificity,
  source order wins)
- Replace TS-style React event types with Flow's Synthetic* and replace
  optional-chain calls with explicit guards so Flow typecheck passes
- Strip keyboard interaction for MVP; mark the handle `aria-hidden="true"`
  so AT users aren't shown an unfocusable widget
- Add `SidebarResizeHandle.test.js` covering render, drag-grow,
  drag-shrink, min/max clamping, dragging class, and listener cleanup
- Add a `resizable` describe block to `Sidebar.test.js` covering FF gate,
  viewport gate, open-state gate, inline-width application, and
  maxWidth-from-viewport behavior
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/elements/content-sidebar/SidebarResizeHandle.js`:
- Around line 58-60: Flow is complaining because setPointerCapture (called on
event.currentTarget in SidebarResizeHandle.js) is typed to accept a string while
event.pointerId is a number; fix by adding a narrow type assertion/cast so Flow
accepts the runtime numeric pointerId (e.g. coerce event.pointerId to any or the
appropriate expected type) before calling event.currentTarget.setPointerCapture,
leaving the runtime value unchanged and keeping the guard that setPointerCapture
exists.
- Around line 38-46: The runtime guards using typeof on event.target should be
replaced by an instanceof Element check to satisfy Flow's typing: in the pointer
event handler inside SidebarResizeHandle (where event.target, event.pointerId,
hasPointerCapture and releasePointerCapture are used), first assert that
event.target instanceof Element, then call
target.hasPointerCapture(event.pointerId) and
target.releasePointerCapture(event.pointerId) — removing the typeof checks so
Flow knows target is an Element before calling those methods.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 07925335-2612-4e8b-9552-cd6af9208e25

📥 Commits

Reviewing files that changed from the base of the PR and between c89d504 and 94a887a.

📒 Files selected for processing (7)
  • src/elements/content-sidebar/Sidebar.js
  • src/elements/content-sidebar/SidebarContent.scss
  • src/elements/content-sidebar/SidebarResizeHandle.js
  • src/elements/content-sidebar/SidebarResizeHandle.scss
  • src/elements/content-sidebar/__tests__/Sidebar.test.js
  • src/elements/content-sidebar/__tests__/SidebarResizeHandle.test.js
  • src/elements/content-sidebar/activity-feed/activity-feed/ActivityFeed.scss
✅ Files skipped from review due to trivial changes (1)
  • src/elements/content-sidebar/SidebarContent.scss

Comment thread src/elements/content-sidebar/SidebarResizeHandle.js Outdated
Comment thread src/elements/content-sidebar/SidebarResizeHandle.js
Flow's lib doesn't refine `EventTarget` to a type with pointer-capture
methods, so `typeof === 'function'` checks weren't enough. Switch to an
`instanceof Element` refinement (which Flow understands) for
`releasePointerCapture`, with a small `(target: any)` escape for
`hasPointerCapture` — Flow's `Element` lib declares the former but not
the latter.

Also cast `event.pointerId` (number) to string when passing to the
pointer-capture methods: Flow types them as `(string)` even though the
real DOM API takes a number.

Keep the `typeof setPointerCapture === 'function'` guard at runtime —
jsdom doesn't implement it, so dropping the guard breaks unit tests.

&:hover,
&.bcs-resize-handle-is-dragging {
background-color: $bdl-box-blue;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

can we use Blueprint here?

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