From a679fe2c376ed83e4490209d05e91f94b5951e58 Mon Sep 17 00:00:00 2001 From: Jose Gaston Date: Fri, 8 May 2026 12:15:33 -0700 Subject: [PATCH 1/2] feat(content-sidebar): add drag-to-resize handle to sidebar [spike] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduces a pointer- and keyboard-accessible resize handle on the left edge of the sidebar. Current default width becomes the minimum; maximum is clamped at 60% of the viewport. Width state lives on the `Sidebar` component and is session-only (no persistence). 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. Currently gated behind a mocked `const isResizable = true` — replace with a real feature flag check before shipping. --- src/elements/content-sidebar/Sidebar.js | 47 +++++++- .../content-sidebar/SidebarResizeHandle.js | 109 ++++++++++++++++++ 2 files changed, 155 insertions(+), 1 deletion(-) create mode 100644 src/elements/content-sidebar/SidebarResizeHandle.js diff --git a/src/elements/content-sidebar/Sidebar.js b/src/elements/content-sidebar/Sidebar.js index 1bdf94f62e..be5ffc15e1 100644 --- a/src/elements/content-sidebar/Sidebar.js +++ b/src/elements/content-sidebar/Sidebar.js @@ -16,6 +16,7 @@ import LoadingIndicator from '../../components/loading-indicator/LoadingIndicato import LocalStore from '../../utils/LocalStore'; import SidebarNav from './SidebarNav'; import SidebarPanels from './SidebarPanels'; +import SidebarResizeHandle from './SidebarResizeHandle'; import SidebarUtils from './SidebarUtils'; // $FlowFixMe TypeScript file import ThemingStyles from '../common/theming'; @@ -80,6 +81,7 @@ type Props = { type State = { isDirty: boolean, + width: ?number, }; export const SIDEBAR_FORCE_KEY: 'bcs.force' = 'bcs.force'; @@ -87,6 +89,12 @@ export const SIDEBAR_FORCE_VALUE_CLOSED: 'closed' = 'closed'; export const SIDEBAR_FORCE_VALUE_OPEN: 'open' = 'open'; export const SIDEBAR_SELECTED_PANEL_KEY: 'sidebar-selected-panel' = 'sidebar-selected-panel'; +// Resize constants — defaults mirror the hardcoded SCSS values ($sidebarTabsWidth + $sidebarContent[Increased]Width). +// When the resizable feature flag is on, these become the minimum drag-to-resize values. +const SIDEBAR_DEFAULT_WIDTH = 400; +const SIDEBAR_DEFAULT_WIDTH_WIDER = 440; +const SIDEBAR_MAX_WIDTH_RATIO = 0.6; // cap at 60% of viewport width + class Sidebar extends React.Component { static defaultProps = { annotatorState: {}, @@ -111,11 +119,24 @@ class Sidebar extends React.Component { this.state = { isDirty: this.getLocationState('open') || false, + width: null, }; this.setForcedByLocation(); } + /** + * Default sidebar width based on whether the "wider" (Box AI) variant is active. + * Mirrors the SCSS fallback so flipping the flag on doesn't change the rendered width at rest. + */ + getDefaultWidth(hasNativeBoxAISidebar: boolean, hasCustomBoxAISidebar: boolean): number { + return hasNativeBoxAISidebar || hasCustomBoxAISidebar ? SIDEBAR_DEFAULT_WIDTH_WIDER : SIDEBAR_DEFAULT_WIDTH; + } + + handleResize = (width: number): void => { + this.setState({ width }); + }; + componentDidMount() { const { file, api, metadataSidebarProps, docGenSidebarProps, onOpenChange = noop }: Props = this.props; // if docgen feature is enabled, load metadata to check whether file is a docgen template @@ -320,6 +341,7 @@ class Sidebar extends React.Component { theme, versionsSidebarProps, }: Props = this.props; + const { width }: State = this.state; const isOpen = this.isOpen(); const hasCustomBoxAISidebar = customSidebarPanels.some(panel => panel.id === SIDEBAR_VIEW_BOXAI); @@ -331,14 +353,37 @@ class Sidebar extends React.Component { const hasMetadata = SidebarUtils.shouldRenderMetadataSidebar(this.props, metadataEditors); const hasSkills = SidebarUtils.shouldRenderSkillsSidebar(this.props, file); const onVersionHistoryClick = hasVersions ? this.handleVersionHistoryClick : this.props.onVersionHistoryClick; + + // SPIKE: mocked true. Before shipping, destructure `features` from props and replace with + // `isFeatureEnabled(features, 'contentSidebar.resizable.enabled')`. + const isResizable = true; + const minWidth = this.getDefaultWidth(hasNativeBoxAISidebar, hasCustomBoxAISidebar); + const maxWidth = + typeof window !== 'undefined' + ? Math.max(minWidth, Math.round(window.innerWidth * SIDEBAR_MAX_WIDTH_RATIO)) + : minWidth; + const currentWidth = width != null ? Math.min(Math.max(width, minWidth), maxWidth) : minWidth; + // Only force inline width once the user has actually dragged — otherwise leave the SCSS defaults in place. + const shouldApplyInlineWidth = isResizable && isOpen && width != null; + const inlineStyle = shouldApplyInlineWidth ? { width: currentWidth, maxWidth: currentWidth } : undefined; + const styleClassName = classNames('be bcs', className, { 'bcs-is-open': isOpen, + 'bcs-is-resizable': isResizable, 'bcs-is-wider': hasNativeBoxAISidebar || hasCustomBoxAISidebar, }); const defaultPanel = this.getDefaultPanel(); return ( -