From c89d504fffda799c8bbb0027cc2cdf7c29777245 Mon Sep 17 00:00:00 2001 From: Jose Gaston Date: Thu, 14 May 2026 17:48:30 -0400 Subject: [PATCH 1/3] feat(content-sidebar): add drag-to-resize handle behind feature flag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../content-sidebar/ContentSidebar.scss | 22 +++- src/elements/content-sidebar/Sidebar.js | 54 ++++++++- .../content-sidebar/SidebarContent.scss | 10 ++ .../content-sidebar/SidebarResizeHandle.js | 109 ++++++++++++++++++ .../content-sidebar/SidebarResizeHandle.scss | 25 ++++ .../activity-feed/ActivityFeed.scss | 6 + 6 files changed, 223 insertions(+), 3 deletions(-) create mode 100644 src/elements/content-sidebar/SidebarResizeHandle.js create mode 100644 src/elements/content-sidebar/SidebarResizeHandle.scss diff --git a/src/elements/content-sidebar/ContentSidebar.scss b/src/elements/content-sidebar/ContentSidebar.scss index b16a8b098f..18290d0127 100644 --- a/src/elements/content-sidebar/ContentSidebar.scss +++ b/src/elements/content-sidebar/ContentSidebar.scss @@ -8,6 +8,7 @@ $sidebarDefaultErrorIncreasedWidth: $sidebarContentIncreasedWidth - 16px; .be { &.bcs { + position: relative; display: flex; width: auto; min-width: $sidebarTabsWidth; @@ -17,6 +18,16 @@ $sidebarDefaultErrorIncreasedWidth: $sidebarContentIncreasedWidth - 16px; &.bcs-is-wider { max-width: $sidebarIncreasedWidth; } + + // When resizable, the default min-width is the un-resized sidebar width. + // Inline style on the aside element overrides the default max-width to allow grow. + &.bcs-is-resizable.bcs-is-open { + min-width: $sidebarWidth; + + &.bcs-is-wider { + min-width: $sidebarIncreasedWidth; + } + } } .bcs-loading { @@ -61,11 +72,20 @@ $sidebarDefaultErrorIncreasedWidth: $sidebarContentIncreasedWidth - 16px; min-width: 0; max-width: none; max-height: 48px; - transition: max-height .5s ease-in-out 0s; + transition: max-height 0.5s ease-in-out 0s; &.bcs-is-wider { max-width: none; } + + // Neutralize resizable overrides on small screens — sidebar is a bottom sheet here + &.bcs-is-resizable.bcs-is-open { + min-width: 0; + + &.bcs-is-wider { + min-width: 0; + } + } } &.bcs-is-open { diff --git a/src/elements/content-sidebar/Sidebar.js b/src/elements/content-sidebar/Sidebar.js index 1bdf94f62e..d851610cd1 100644 --- a/src/elements/content-sidebar/Sidebar.js +++ b/src/elements/content-sidebar/Sidebar.js @@ -14,8 +14,11 @@ import { withRouter } from 'react-router-dom'; import type { Location, RouterHistory } from 'react-router-dom'; import LoadingIndicator from '../../components/loading-indicator/LoadingIndicator'; import LocalStore from '../../utils/LocalStore'; +import withMediaQuery from '../../components/media-query/withMediaQuery'; +import { VIEW_SIZE_TYPE } from '../../components/media-query/constants'; import SidebarNav from './SidebarNav'; import SidebarPanels from './SidebarPanels'; +import SidebarResizeHandle from './SidebarResizeHandle'; import SidebarUtils from './SidebarUtils'; // $FlowFixMe TypeScript file import ThemingStyles from '../common/theming'; @@ -74,12 +77,14 @@ type Props = { /** When true, enables data fetching. When false, defers data fetching. Used to prioritize preview loading. */ shouldFetchSidebarData?: boolean, signSidebarProps: SignSidebarProps, + size: $Values, theme?: Theme, versionsSidebarProps: VersionsSidebarProps, }; type State = { isDirty: boolean, + width: ?number, }; export const SIDEBAR_FORCE_KEY: 'bcs.force' = 'bcs.force'; @@ -87,6 +92,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.5; // cap at 50% of viewport width + class Sidebar extends React.Component { static defaultProps = { annotatorState: {}, @@ -111,11 +122,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 @@ -304,6 +328,7 @@ class Sidebar extends React.Component { customSidebarPanels = [], detailsSidebarProps, docGenSidebarProps, + features, file, fileId, getPreview, @@ -317,9 +342,11 @@ class Sidebar extends React.Component { onAnnotationSelect, onVersionChange, signSidebarProps, + size, 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 +358,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; + + const isViewportWideEnoughToResize = size === VIEW_SIZE_TYPE.large || size === VIEW_SIZE_TYPE.xlarge; + const isResizable = + isFeatureEnabled(features, 'contentSidebar.resizable.enabled') && isViewportWideEnoughToResize; + 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 ( -