Skip to content

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

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

feat(content-sidebar): add drag-to-resize handle behind feature flag#4559
jmcbgaston wants to merge 3 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

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

Walkthrough

Adds a pointer-driven SidebarResizeHandle, wires resizable behavior behind a feature flag and viewport check in Sidebar, stores/clamps user width in state, 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, 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 Resize tests (unit)
src/elements/content-sidebar/__tests__/SidebarResizeHandle.test.js
Jest 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.
Sidebar integration tests
src/elements/content-sidebar/__tests__/Sidebar.test.js
Adds resizable tests validating feature-flag + viewport gating, bcs-is-resizable class and handle presence, and inline aside width/maxWidth behavior after resize (maxWidth capped to 50% of viewWidth).
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, refines .bcs-is-resizable.bcs-is-open min-width rules (including smaller-screen neutralization), forces .bcs-content/.bcs-scroll-content and activity feed to width: 100% when resizable, and removes the old .bcs-is-wider activity-feed width rule.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested labels

ready-to-merge

Suggested reviewers

  • tjiang-box
  • jpan-box
  • tjuanitas

Poem

🐰 I nudged the handle with a curious paw,
The sidebar stretched wide — hooray, hurrah!
Drag left, drag right, a smooth little slide,
Width clamps in place, no surprises abide.
Happy hops for layout, snug and awry.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Description check ❓ Inconclusive PR description is mostly complete with clear objectives, test plan, and semantic release type, but conflicts with actual implementation. The description states keyboard support and ARIA accessibility (role='separator', aria-valuenow, aria-valuemin, aria-valuemax), but the implementation uses aria-hidden='true' with no keyboard handlers. Clarify whether keyboard support is deferred and update description to match current implementation.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main feature: adding a drag-to-resize handle for the content sidebar, gated behind a feature flag. It is specific and clear.
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 on lines +38 to +46
const {target} = event;
if (
target &&
typeof target.hasPointerCapture === 'function' &&
typeof target.releasePointerCapture === 'function' &&
target.hasPointerCapture(event.pointerId)
) {
target.releasePointerCapture(event.pointerId);
}
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.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check Flow's type definitions for pointer capture APIs
rg -A5 "hasPointerCapture|releasePointerCapture" flow-typed/ node_modules/flow-bin/

Repository: box/box-ui-elements

Length of output: 126


🏁 Script executed:

#!/bin/bash
# First, locate the file and examine its current state
find . -name "SidebarResizeHandle.js" -type f

Repository: box/box-ui-elements

Length of output: 117


🏁 Script executed:

#!/bin/bash
# Check Flow configuration and type definitions
git ls-files | grep -E "flow|\.flowconfig" | head -20

Repository: box/box-ui-elements

Length of output: 802


🏁 Script executed:

#!/bin/bash
# Look for Flow type definitions related to pointer events and Element
rg -l "PointerEvent|hasPointerCapture" --type ts --type tsx --type js --type jsx | head -20

Repository: box/box-ui-elements

Length of output: 91


🏁 Script executed:

#!/bin/bash
# Examine the actual SidebarResizeHandle.js file
cat -n src/elements/content-sidebar/SidebarResizeHandle.js

Repository: box/box-ui-elements

Length of output: 3548


🏁 Script executed:

#!/bin/bash
# Check Flow configuration
cat -n .flowconfig

Repository: box/box-ui-elements

Length of output: 3768


🏁 Script executed:

#!/bin/bash
# Search for PointerEvent type definitions in Flow typed files
rg "PointerEvent" flow-typed/

Repository: box/box-ui-elements

Length of output: 45


🏁 Script executed:

#!/bin/bash
# Look for PointerEvent and Element type definitions in Flow stubs
rg "Element|PointerEvent" flow-typed/ --type js

Repository: box/box-ui-elements

Length of output: 2939


🏁 Script executed:

#!/bin/bash
# Check the React synthetic event type definitions
rg "SyntheticPointerEvent" . --type js -B 2 -A 5 | head -40

Repository: box/box-ui-elements

Length of output: 803


🏁 Script executed:

#!/bin/bash
# Search for other uses of pointer capture methods in the codebase
rg "hasPointerCapture|releasePointerCapture|setPointerCapture" . --type js -B 2 -A 2

Repository: box/box-ui-elements

Length of output: 1568


🏁 Script executed:

#!/bin/bash
# Check if there are any Flow suppression comments in this file
grep -n "FlowFixMe\|flowfixme\|TODO.*Flow" src/elements/content-sidebar/SidebarResizeHandle.js

Repository: box/box-ui-elements

Length of output: 45


🏁 Script executed:

#!/bin/bash
# Verify Flow's built-in type definitions for Element and EventTarget
# Check if Flow includes Pointer Capture in the standard library
rg "hasPointerCapture" . --type js --type flow 2>/dev/null | grep -v node_modules | head -20

Repository: box/box-ui-elements

Length of output: 45


Use instanceof Element check instead of typeof guards for pointer capture methods.

Flow's type system correctly identifies that EventTarget (the type of event.target in a PointerEvent) doesn't have hasPointerCapture and releasePointerCapture methods—these exist only on Element. The current typeof checks work at runtime but don't satisfy Flow's static type analysis.

Replace the runtime guards with an instanceof Element check to properly narrow the type:

Proposed fix
const handlePointerUp = React.useCallback(
    (event: PointerEvent) => {
        setIsDragging(false);
-       const {target} = event;
+       const target = event.target;
        if (
-           target &&
-           typeof target.hasPointerCapture === 'function' &&
-           typeof target.releasePointerCapture === 'function' &&
+           target instanceof Element &&
            target.hasPointerCapture(event.pointerId)
        ) {
            target.releasePointerCapture(event.pointerId);
        }
        window.removeEventListener('pointermove', handlePointerMove);
        window.removeEventListener('pointerup', handlePointerUp);
    },
    [handlePointerMove],
);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const {target} = event;
if (
target &&
typeof target.hasPointerCapture === 'function' &&
typeof target.releasePointerCapture === 'function' &&
target.hasPointerCapture(event.pointerId)
) {
target.releasePointerCapture(event.pointerId);
}
const target = event.target;
if (
target instanceof Element &&
target.hasPointerCapture(event.pointerId)
) {
target.releasePointerCapture(event.pointerId);
}
🧰 Tools
🪛 CircleCI: flow

[error] 41-41: Flow error: Property hasPointerCapture is missing in EventTarget [1]. (Property hasPointerCapture is missing in EventTarget)


[error] 42-42: Flow error: Property releasePointerCapture is missing in EventTarget [1]. (Property releasePointerCapture is missing in EventTarget)


[error] 43-43: Flow error: Cannot call target.hasPointerCapture because the parameter types of an unknown function [1] are unknown. (Cannot call target.hasPointerCapture...)


[error] 45-45: Flow error: Cannot call target.releasePointerCapture because the parameter types of an unknown function [1] are unknown. (Cannot call target.releasePointerCapture...)

🤖 Prompt for 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.

In `@src/elements/content-sidebar/SidebarResizeHandle.js` around lines 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.

Comment on lines +58 to +60
if (typeof event.currentTarget.setPointerCapture === 'function') {
event.currentTarget.setPointerCapture(event.pointerId);
}
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.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Flow type error: pointerId type mismatch.

Flow reports that setPointerCapture expects a string but event.pointerId is a number. This is a Flow type definition issue – the DOM PointerEvent API uses numeric pointerIds, but Flow's definition may be incorrect or outdated.

🔧 Proposed fix using type assertion
 const handlePointerDown = (event: SyntheticPointerEvent<HTMLDivElement>) => {
     event.preventDefault();
     startXRef.current = event.clientX;
     startWidthRef.current = width;
     setIsDragging(true);
     if (typeof event.currentTarget.setPointerCapture === 'function') {
-        event.currentTarget.setPointerCapture(event.pointerId);
+        event.currentTarget.setPointerCapture((event.pointerId: any));
     }
     window.addEventListener('pointermove', handlePointerMove);
     window.addEventListener('pointerup', handlePointerUp);
 };

Verify the correct type for pointerId in Flow:

#!/bin/bash
# Check Flow's PointerEvent type definition
rg -A10 "type.*PointerEvent" flow-typed/ node_modules/flow-bin/ | rg "pointerId"
🧰 Tools
🪛 CircleCI: flow

[error] 59-59: Flow error: Cannot call event.currentTarget.setPointerCapture with event.pointerId; number is incompatible with string. (Cannot call setPointerCapture...)

🤖 Prompt for 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.

In `@src/elements/content-sidebar/SidebarResizeHandle.js` around lines 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.

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.

1 participant