From 03cf9ff35a828941f2610ae9e0ad408f3b058f46 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 3 Mar 2026 09:23:44 +0000
Subject: [PATCH 01/17] Initial plan
From 9f380ec7dd7179c13f0f7545e60a98b21fbbb317 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 3 Mar 2026 09:54:02 +0000
Subject: [PATCH 02/17] feat(a11y): make resize handles for addon panel and
sidebar accessible
- Add role="separator", tabIndex, aria-orientation, aria-label, aria-valuenow,
aria-valuemin, aria-valuemax to sidebar and panel drag handles
- Add keyboard interaction (arrow keys, Home/End) for resizing via keyboard
- Add focus-visible styles so drag handles are visible when focused
Co-authored-by: Sidnioulz <5108577+Sidnioulz@users.noreply.github.com>
---
.../src/manager/components/layout/Layout.tsx | 32 ++++++++++-
.../manager/components/layout/useDragging.ts | 54 +++++++++++++++++++
2 files changed, 84 insertions(+), 2 deletions(-)
diff --git a/code/core/src/manager/components/layout/Layout.tsx b/code/core/src/manager/components/layout/Layout.tsx
index f1b019d5a3d3..47c365bc39a7 100644
--- a/code/core/src/manager/components/layout/Layout.tsx
+++ b/code/core/src/manager/components/layout/Layout.tsx
@@ -1,4 +1,4 @@
-import type { CSSProperties, FC } from 'react';
+import type { CSSProperties } from 'react';
import React, { useEffect, useLayoutEffect, useState } from 'react';
import type { API_Layout, API_ViewMode } from 'storybook/internal/types';
@@ -175,7 +175,16 @@ export const Layout = ({ managerLayoutState, setManagerLayoutState, hasTab, ...s
<>
{isDesktop && (
-
+
{slots.slotSidebar}
)}
@@ -200,6 +209,25 @@ export const Layout = ({ managerLayoutState, setManagerLayoutState, hasTab, ...s
panelResizerRef={panelResizerRef}
position={panelPosition}
>
+
{slots.slotPanel}
)}
diff --git a/code/core/src/manager/components/layout/useDragging.ts b/code/core/src/manager/components/layout/useDragging.ts
index 1028f45df9f5..2de46c16067c 100644
--- a/code/core/src/manager/components/layout/useDragging.ts
+++ b/code/core/src/manager/components/layout/useDragging.ts
@@ -8,6 +8,7 @@ const SNAP_THRESHOLD_PX = 30;
const SIDEBAR_MIN_WIDTH_PX = 240;
const RIGHT_PANEL_MIN_WIDTH_PX = 270;
const MIN_WIDTH_STIFFNESS = 0.9;
+const KEYBOARD_STEP_PX = 10;
/** Clamps a value between min and max. */
function clamp(value: number, min: number, max: number): number {
@@ -172,12 +173,65 @@ export function useDragging({
});
};
+ const onSidebarKeyDown = (e: KeyboardEvent) => {
+ if (!['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(e.key)) {
+ return;
+ }
+ e.preventDefault();
+ setState((state) => {
+ const maxSize = window.innerWidth;
+ switch (e.key) {
+ case 'ArrowRight':
+ return { ...state, navSize: clamp(state.navSize + KEYBOARD_STEP_PX, 0, maxSize) };
+ case 'ArrowLeft':
+ return { ...state, navSize: clamp(state.navSize - KEYBOARD_STEP_PX, 0, maxSize) };
+ case 'Home':
+ return { ...state, navSize: 0 };
+ case 'End':
+ return { ...state, navSize: maxSize };
+ default:
+ return state;
+ }
+ });
+ };
+
+ const onPanelKeyDown = (e: KeyboardEvent) => {
+ if (!['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End'].includes(e.key)) {
+ return;
+ }
+ e.preventDefault();
+ setState((state) => {
+ const isBottom = state.panelPosition === 'bottom';
+ const sizeKey = isBottom ? 'bottomPanelHeight' : 'rightPanelWidth';
+ const maxSize = isBottom ? window.innerHeight : window.innerWidth;
+ const increaseKey = isBottom ? 'ArrowUp' : 'ArrowLeft';
+ const decreaseKey = isBottom ? 'ArrowDown' : 'ArrowRight';
+
+ switch (e.key) {
+ case increaseKey:
+ return { ...state, [sizeKey]: clamp(state[sizeKey] + KEYBOARD_STEP_PX, 0, maxSize) };
+ case decreaseKey:
+ return { ...state, [sizeKey]: clamp(state[sizeKey] - KEYBOARD_STEP_PX, 0, maxSize) };
+ case 'Home':
+ return { ...state, [sizeKey]: 0 };
+ case 'End':
+ return { ...state, [sizeKey]: maxSize };
+ default:
+ return state;
+ }
+ });
+ };
+
panelResizer?.addEventListener('mousedown', onDragStart);
sidebarResizer?.addEventListener('mousedown', onDragStart);
+ panelResizer?.addEventListener('keydown', onPanelKeyDown);
+ sidebarResizer?.addEventListener('keydown', onSidebarKeyDown);
return () => {
panelResizer?.removeEventListener('mousedown', onDragStart);
sidebarResizer?.removeEventListener('mousedown', onDragStart);
+ panelResizer?.removeEventListener('keydown', onPanelKeyDown);
+ sidebarResizer?.removeEventListener('keydown', onSidebarKeyDown);
// make iframe capture pointer events again
previewIframe?.removeAttribute('style');
};
From 8e0fbf5346d305d2b5f2acbed4f81918493418fc Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 4 Mar 2026 13:42:58 +0000
Subject: [PATCH 03/17] refactor(a11y): address review feedback on resize
handles
- Refactor keyboard handlers into shared applyResizeKeyboard function
with position parameter (left/right/bottom/top)
- Add Shift key support for 5x step size
- Move slotSidebar before Drag handle in sidebar container for
better keyboard navigation order
- Update focus-visible styles with outline:none, height:7,
and boxShadow for the drag handle
Co-authored-by: Sidnioulz <5108577+Sidnioulz@users.noreply.github.com>
---
.../src/manager/components/layout/Layout.tsx | 2 +-
.../manager/components/layout/useDragging.ts | 108 ++++++++++++------
2 files changed, 72 insertions(+), 38 deletions(-)
diff --git a/code/core/src/manager/components/layout/Layout.tsx b/code/core/src/manager/components/layout/Layout.tsx
index 47c365bc39a7..071077cdb9be 100644
--- a/code/core/src/manager/components/layout/Layout.tsx
+++ b/code/core/src/manager/components/layout/Layout.tsx
@@ -175,6 +175,7 @@ export const Layout = ({ managerLayoutState, setManagerLayoutState, hasTab, ...s
<>
{isDesktop && (
+ {slots.slotSidebar}
- {slots.slotSidebar}
)}
{isMobile && (
diff --git a/code/core/src/manager/components/layout/useDragging.ts b/code/core/src/manager/components/layout/useDragging.ts
index 2de46c16067c..831aa8b034e2 100644
--- a/code/core/src/manager/components/layout/useDragging.ts
+++ b/code/core/src/manager/components/layout/useDragging.ts
@@ -9,6 +9,8 @@ const SIDEBAR_MIN_WIDTH_PX = 240;
const RIGHT_PANEL_MIN_WIDTH_PX = 270;
const MIN_WIDTH_STIFFNESS = 0.9;
const KEYBOARD_STEP_PX = 10;
+const KEYBOARD_SHIFT_MULTIPLIER = 5;
+const RESIZE_KEYS = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End'];
/** Clamps a value between min and max. */
function clamp(value: number, min: number, max: number): number {
@@ -20,6 +22,65 @@ function interpolate(relativeValue: number, min: number, max: number): number {
return min + (max - min) * relativeValue;
}
+/**
+ * Given the current layout state, the position of the resize handle, and the key pressed,
+ * returns the next layout state with the resized panel/sidebar.
+ *
+ * @param position - The position of the resize handle relative to the content it resizes.
+ * 'left' for sidebar, 'bottom' or 'right' for the addon panel.
+ */
+function applyResizeKeyboard(
+ state: LayoutState,
+ position: 'left' | 'right' | 'bottom' | 'top',
+ key: string,
+ step: number
+): LayoutState {
+ const sizeKey =
+ position === 'left'
+ ? 'navSize'
+ : position === 'bottom' || position === 'top'
+ ? 'bottomPanelHeight'
+ : 'rightPanelWidth';
+ const maxSize =
+ position === 'bottom' || position === 'top' ? window.innerHeight : window.innerWidth;
+
+ let increaseKey: string;
+ let decreaseKey: string;
+ switch (position) {
+ case 'left':
+ increaseKey = 'ArrowRight';
+ decreaseKey = 'ArrowLeft';
+ break;
+ case 'right':
+ increaseKey = 'ArrowLeft';
+ decreaseKey = 'ArrowRight';
+ break;
+ case 'bottom':
+ increaseKey = 'ArrowUp';
+ decreaseKey = 'ArrowDown';
+ break;
+ case 'top':
+ increaseKey = 'ArrowDown';
+ decreaseKey = 'ArrowUp';
+ break;
+ }
+
+ const currentSize = state[sizeKey];
+
+ switch (key) {
+ case increaseKey:
+ return { ...state, [sizeKey]: clamp(currentSize + step, 0, maxSize) };
+ case decreaseKey:
+ return { ...state, [sizeKey]: clamp(currentSize - step, 0, maxSize) };
+ case 'Home':
+ return { ...state, [sizeKey]: 0 };
+ case 'End':
+ return { ...state, [sizeKey]: maxSize };
+ default:
+ return state;
+ }
+}
+
export function useDragging({
setState,
isPanelShown,
@@ -174,52 +235,25 @@ export function useDragging({
};
const onSidebarKeyDown = (e: KeyboardEvent) => {
- if (!['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(e.key)) {
+ if (!RESIZE_KEYS.includes(e.key)) {
return;
}
e.preventDefault();
- setState((state) => {
- const maxSize = window.innerWidth;
- switch (e.key) {
- case 'ArrowRight':
- return { ...state, navSize: clamp(state.navSize + KEYBOARD_STEP_PX, 0, maxSize) };
- case 'ArrowLeft':
- return { ...state, navSize: clamp(state.navSize - KEYBOARD_STEP_PX, 0, maxSize) };
- case 'Home':
- return { ...state, navSize: 0 };
- case 'End':
- return { ...state, navSize: maxSize };
- default:
- return state;
- }
- });
+ const step = e.shiftKey
+ ? KEYBOARD_STEP_PX * KEYBOARD_SHIFT_MULTIPLIER
+ : KEYBOARD_STEP_PX;
+ setState((state) => applyResizeKeyboard(state, 'left', e.key, step));
};
const onPanelKeyDown = (e: KeyboardEvent) => {
- if (!['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End'].includes(e.key)) {
+ if (!RESIZE_KEYS.includes(e.key)) {
return;
}
e.preventDefault();
- setState((state) => {
- const isBottom = state.panelPosition === 'bottom';
- const sizeKey = isBottom ? 'bottomPanelHeight' : 'rightPanelWidth';
- const maxSize = isBottom ? window.innerHeight : window.innerWidth;
- const increaseKey = isBottom ? 'ArrowUp' : 'ArrowLeft';
- const decreaseKey = isBottom ? 'ArrowDown' : 'ArrowRight';
-
- switch (e.key) {
- case increaseKey:
- return { ...state, [sizeKey]: clamp(state[sizeKey] + KEYBOARD_STEP_PX, 0, maxSize) };
- case decreaseKey:
- return { ...state, [sizeKey]: clamp(state[sizeKey] - KEYBOARD_STEP_PX, 0, maxSize) };
- case 'Home':
- return { ...state, [sizeKey]: 0 };
- case 'End':
- return { ...state, [sizeKey]: maxSize };
- default:
- return state;
- }
- });
+ const step = e.shiftKey
+ ? KEYBOARD_STEP_PX * KEYBOARD_SHIFT_MULTIPLIER
+ : KEYBOARD_STEP_PX;
+ setState((state) => applyResizeKeyboard(state, state.panelPosition, e.key, step));
};
panelResizer?.addEventListener('mousedown', onDragStart);
From 2082668095fabc031b9a64ca3a841c98e2797c77 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 9 Mar 2026 07:53:28 +0000
Subject: [PATCH 04/17] refactor(a11y): rework resize handles based on next
branch structure
- Extract Drag.tsx component with built-in role=separator, tabIndex,
aria-valuemin, and TooltipProvider (focus-only) showing arrow keys
- Extract PanelContainer.tsx with ARIA attributes and hidden/aria-hidden
for collapsed panel content
- Extract MainAreaContainer.tsx matching next branch structure
- Simplify Layout.tsx to use extracted components, reducing markup
- Fix focus-visible: use height when horizontal, width when vertical
- Sidebar slot stays before Drag handle for keyboard nav order
Co-authored-by: Sidnioulz <5108577+Sidnioulz@users.noreply.github.com>
---
.../src/manager/components/layout/Drag.tsx | 88 ++++++++++++++++++-
.../src/manager/components/layout/Layout.tsx | 22 -----
.../components/layout/PanelContainer.tsx | 10 +++
.../manager/components/layout/useDragging.ts | 16 ++--
4 files changed, 100 insertions(+), 36 deletions(-)
diff --git a/code/core/src/manager/components/layout/Drag.tsx b/code/core/src/manager/components/layout/Drag.tsx
index ba1fe4053d3d..8ab18a22c3f7 100644
--- a/code/core/src/manager/components/layout/Drag.tsx
+++ b/code/core/src/manager/components/layout/Drag.tsx
@@ -1,12 +1,84 @@
+import React, { forwardRef, useMemo } from 'react';
+
import { styled } from 'storybook/theming';
+import { TooltipNote } from '../../../components/components/tooltip/TooltipNote';
+import { TooltipProvider } from '../../../components/components/tooltip/TooltipProvider';
+
+interface DragProps {
+ /** Visual orientation of the drag handle line. */
+ orientation?: 'horizontal' | 'vertical';
+
+ /** Whether the drag handle overlaps the adjacent content area. */
+ overlapping?: boolean;
+
+ /** Which side the drag handle sits on, relative to the content it resizes. */
+ position?: 'left' | 'right';
+
+ /** Accessible orientation for the separator role (determines arrow key mapping). */
+ 'aria-orientation'?: 'horizontal' | 'vertical';
+
+ /** Accessible label describing what this separator resizes. */
+ 'aria-label': string;
+
+ /** Current size (in pixels) of the region controlled by this separator. */
+ 'aria-valuenow': number;
+
+ /** Maximum size (in pixels) for the region controlled by this separator. */
+ 'aria-valuemax'?: number;
+}
+
/**
* Drag handle for the sidebar and panel resizers. Can be horizontal (bottom panel) or vertical
- * (sidebar or right panel). Can optionally be set to not overlap the content area (only render
- * outside of it), which is necessary when the panel is collapsed to prevent a layout shift when
- * scrollIntoView is used.
+ * (sidebar or right panel). Implements the WAI-ARIA separator role with keyboard resize support.
+ *
+ * The component automatically sets `role="separator"`, `tabIndex={0}`, and `aria-valuemin={0}`. A
+ * tooltip is shown on focus advertising the arrow keys available for keyboard resizing.
*/
-export const Drag = styled.div<{
+export const Drag = forwardRef(function Drag(props, ref) {
+ const {
+ orientation,
+ overlapping,
+ position,
+ 'aria-orientation': ariaOrientation = 'horizontal',
+ 'aria-label': ariaLabel,
+ 'aria-valuenow': ariaValueNow,
+ 'aria-valuemax': ariaValueMax,
+ ...rest
+ } = props;
+
+ const tooltipNote = useMemo(() => {
+ if (ariaOrientation === 'vertical') {
+ return '← → to resize';
+ }
+ return '↑ ↓ to resize';
+ }, [ariaOrientation]);
+
+ return (
+ }
+ >
+
+
+ );
+});
+
+const DragHandle = styled.div<{
orientation?: 'horizontal' | 'vertical';
overlapping?: boolean;
position?: 'left' | 'right';
@@ -27,6 +99,14 @@ export const Drag = styled.div<{
opacity: 1,
},
}),
+ ({ theme, orientation = 'vertical' }) => ({
+ '&:focus-visible': {
+ opacity: 1,
+ outline: 'none',
+ ...(orientation === 'horizontal' ? { height: 7 } : { width: 7 }),
+ boxShadow: `inset 0 0 0 4px ${theme.color.secondary}`,
+ },
+ }),
({ orientation = 'vertical', overlapping = true, position = 'left' }) =>
orientation === 'vertical'
? {
diff --git a/code/core/src/manager/components/layout/Layout.tsx b/code/core/src/manager/components/layout/Layout.tsx
index 071077cdb9be..d3d4a18743c2 100644
--- a/code/core/src/manager/components/layout/Layout.tsx
+++ b/code/core/src/manager/components/layout/Layout.tsx
@@ -178,12 +178,9 @@ export const Layout = ({ managerLayoutState, setManagerLayoutState, hasTab, ...s
{slots.slotSidebar}
@@ -209,25 +206,6 @@ export const Layout = ({ managerLayoutState, setManagerLayoutState, hasTab, ...s
panelResizerRef={panelResizerRef}
position={panelPosition}
>
-
{slots.slotPanel}
)}
diff --git a/code/core/src/manager/components/layout/PanelContainer.tsx b/code/core/src/manager/components/layout/PanelContainer.tsx
index 9ddce06d569e..d529b9783e65 100644
--- a/code/core/src/manager/components/layout/PanelContainer.tsx
+++ b/code/core/src/manager/components/layout/PanelContainer.tsx
@@ -43,6 +43,16 @@ const PanelContainer = React.memo(function PanelContainer(p
overlapping={position === 'bottom' ? !!bottomPanelHeight : !!rightPanelWidth}
position={position === 'bottom' ? 'left' : 'right'}
ref={panelResizerRef}
+ aria-orientation={position === 'bottom' ? 'horizontal' : 'vertical'}
+ aria-label="Addon panel resize handle"
+ aria-valuenow={position === 'bottom' ? bottomPanelHeight : rightPanelWidth}
+ aria-valuemax={
+ typeof window !== 'undefined'
+ ? position === 'bottom'
+ ? window.innerHeight
+ : window.innerWidth
+ : undefined
+ }
/>
applyResizeKeyboard(state, 'left', e.key, step));
};
@@ -250,9 +248,7 @@ export function useDragging({
return;
}
e.preventDefault();
- const step = e.shiftKey
- ? KEYBOARD_STEP_PX * KEYBOARD_SHIFT_MULTIPLIER
- : KEYBOARD_STEP_PX;
+ const step = e.shiftKey ? KEYBOARD_STEP_PX * KEYBOARD_SHIFT_MULTIPLIER : KEYBOARD_STEP_PX;
setState((state) => applyResizeKeyboard(state, state.panelPosition, e.key, step));
};
From b5b2969a971c8552330731c4aa3c62fa838b4e33 Mon Sep 17 00:00:00 2001
From: Steve Dodier-Lazaro
Date: Mon, 9 Mar 2026 10:59:50 +0100
Subject: [PATCH 05/17] Refactor smelly code and rebase leftovers
---
.../src/manager/components/layout/Drag.tsx | 65 ++++++++++---------
.../src/manager/components/layout/Layout.tsx | 10 ++-
.../components/layout/PanelContainer.tsx | 6 +-
3 files changed, 41 insertions(+), 40 deletions(-)
diff --git a/code/core/src/manager/components/layout/Drag.tsx b/code/core/src/manager/components/layout/Drag.tsx
index 8ab18a22c3f7..1b14462bbf8a 100644
--- a/code/core/src/manager/components/layout/Drag.tsx
+++ b/code/core/src/manager/components/layout/Drag.tsx
@@ -2,22 +2,17 @@ import React, { forwardRef, useMemo } from 'react';
import { styled } from 'storybook/theming';
+import type { PopperPlacement } from '../../../components';
import { TooltipNote } from '../../../components/components/tooltip/TooltipNote';
import { TooltipProvider } from '../../../components/components/tooltip/TooltipProvider';
interface DragProps {
- /** Visual orientation of the drag handle line. */
- orientation?: 'horizontal' | 'vertical';
+ /** Which side the drag handle sits on, relative to the content it resizes. Determines orientation. */
+ position: 'left' | 'right' | 'top' | 'bottom';
/** Whether the drag handle overlaps the adjacent content area. */
overlapping?: boolean;
- /** Which side the drag handle sits on, relative to the content it resizes. */
- position?: 'left' | 'right';
-
- /** Accessible orientation for the separator role (determines arrow key mapping). */
- 'aria-orientation'?: 'horizontal' | 'vertical';
-
/** Accessible label describing what this separator resizes. */
'aria-label': string;
@@ -28,6 +23,13 @@ interface DragProps {
'aria-valuemax'?: number;
}
+const oppositePosition: Record = {
+ left: 'right',
+ right: 'left',
+ top: 'bottom',
+ bottom: 'top',
+};
+
/**
* Drag handle for the sidebar and panel resizers. Can be horizontal (bottom panel) or vertical
* (sidebar or right panel). Implements the WAI-ARIA separator role with keyboard resize support.
@@ -37,37 +39,37 @@ interface DragProps {
*/
export const Drag = forwardRef(function Drag(props, ref) {
const {
- orientation,
overlapping,
position,
- 'aria-orientation': ariaOrientation = 'horizontal',
'aria-label': ariaLabel,
'aria-valuenow': ariaValueNow,
'aria-valuemax': ariaValueMax,
...rest
} = props;
+ const orientation = position === 'left' || position === 'right' ? 'vertical' : 'horizontal';
+
const tooltipNote = useMemo(() => {
- if (ariaOrientation === 'vertical') {
+ if (orientation === 'vertical') {
return '← → to resize';
}
return '↑ ↓ to resize';
- }, [ariaOrientation]);
+ }, [orientation]);
return (
}
>
(function Drag(props, r
});
const DragHandle = styled.div<{
- orientation?: 'horizontal' | 'vertical';
- overlapping?: boolean;
- position?: 'left' | 'right';
+ $orientation?: 'horizontal' | 'vertical';
+ $overlapping?: boolean;
+ $position: 'left' | 'right' | 'top' | 'bottom';
}>(
({ theme }) => ({
position: 'absolute',
@@ -99,27 +101,29 @@ const DragHandle = styled.div<{
opacity: 1,
},
}),
- ({ theme, orientation = 'vertical' }) => ({
+ ({ theme, $orientation = 'vertical' }) => ({
'&:focus-visible': {
opacity: 1,
outline: 'none',
- ...(orientation === 'horizontal' ? { height: 7 } : { width: 7 }),
+ ...($orientation === 'horizontal' ? { height: 7 } : { width: 7 }),
boxShadow: `inset 0 0 0 4px ${theme.color.secondary}`,
},
}),
- ({ orientation = 'vertical', overlapping = true, position = 'left' }) =>
- orientation === 'vertical'
+ ({ $orientation = 'vertical', $overlapping = true, $position = 'left' }) =>
+ $orientation === 'vertical'
? {
- width: overlapping ? (position === 'left' ? 10 : 13) : 7,
+ // This is an old code smell, where 10px matches the sidebar and 13px matches the addon panel.
+ // It should be tidied up at some point.
+ width: $overlapping ? ($position === 'left' ? 10 : 13) : 7,
height: '100%',
top: 0,
- right: position === 'left' ? -7 : undefined,
- left: position === 'right' ? -7 : undefined,
+ right: $position === 'left' ? -7 : undefined,
+ left: $position === 'right' ? -7 : undefined,
'&:after': {
width: 1,
height: '100%',
- marginLeft: position === 'left' ? 3 : 6,
+ marginLeft: $position === 'left' ? 3 : 6,
},
'&:hover': {
@@ -128,8 +132,9 @@ const DragHandle = styled.div<{
}
: {
width: '100%',
- height: overlapping ? 13 : 7,
- top: -7,
+ height: $overlapping ? 13 : 7,
+ top: $position === 'bottom' ? -7 : undefined,
+ bottom: $position === 'top' ? -7 : undefined,
left: 0,
'&:after': {
diff --git a/code/core/src/manager/components/layout/Layout.tsx b/code/core/src/manager/components/layout/Layout.tsx
index d3d4a18743c2..310fa560ebf4 100644
--- a/code/core/src/manager/components/layout/Layout.tsx
+++ b/code/core/src/manager/components/layout/Layout.tsx
@@ -131,9 +131,7 @@ const useLayoutSyncingState = ({
panelResizerRef,
sidebarResizerRef,
showPages: isPagesShown,
- showPanel:
- customisedShowPanel &&
- (managerLayoutState.panelPosition === 'right' ? rightPanelWidth > 0 : bottomPanelHeight > 0),
+ showPanel: customisedShowPanel,
isDragging: internalDraggingSizeState.isDragging,
};
};
@@ -178,7 +176,7 @@ export const Layout = ({ managerLayoutState, setManagerLayoutState, hasTab, ...s
{slots.slotSidebar}
- {isDesktop && showPanel && (
+ {isDesktop && (
- {slots.slotPanel}
+ {showPanel && slots.slotPanel}
)}
{isMobile && }
diff --git a/code/core/src/manager/components/layout/PanelContainer.tsx b/code/core/src/manager/components/layout/PanelContainer.tsx
index d529b9783e65..c97931fcde57 100644
--- a/code/core/src/manager/components/layout/PanelContainer.tsx
+++ b/code/core/src/manager/components/layout/PanelContainer.tsx
@@ -39,11 +39,9 @@ const PanelContainer = React.memo(function PanelContainer(p
return (
Date: Mon, 9 Mar 2026 10:13:30 +0000
Subject: [PATCH 06/17] feat(a11y): add SidebarContainer with focus management
for sidebar toggle
- Create SidebarContainer.tsx with hidden/aria-hidden when navSize === 0
- Add sidebarRegion and showSidebar IDs to focusableUIElements
- Update menu.tsx tool with focus management and landmark animation
- Update shortcuts.ts to move focus to Show sidebar button when closing
- Simplify Layout.tsx to use SidebarContainer component
Co-authored-by: Sidnioulz <5108577+Sidnioulz@users.noreply.github.com>
---
code/core/src/manager-api/modules/layout.ts | 2 +
.../core/src/manager-api/modules/shortcuts.ts | 21 ++++++
.../src/manager/components/layout/Layout.tsx | 18 +----
.../components/layout/SidebarContainer.tsx | 56 +++++++++++++++
.../manager/components/preview/tools/menu.tsx | 68 +++++++++++++------
5 files changed, 127 insertions(+), 38 deletions(-)
create mode 100644 code/core/src/manager/components/layout/SidebarContainer.tsx
diff --git a/code/core/src/manager-api/modules/layout.ts b/code/core/src/manager-api/modules/layout.ts
index a0df07067e5b..a5b47b2b432f 100644
--- a/code/core/src/manager-api/modules/layout.ts
+++ b/code/core/src/manager-api/modules/layout.ts
@@ -153,6 +153,8 @@ export const focusableUIElements = {
storyListMenu: 'storybook-explorer-menu',
storyPanelRoot: 'storybook-panel-root',
showAddonPanel: 'storybook-show-addon-panel',
+ sidebarRegion: 'storybook-sidebar-region',
+ showSidebar: 'storybook-show-sidebar',
};
const getIsNavShown = (state: State) => {
diff --git a/code/core/src/manager-api/modules/shortcuts.ts b/code/core/src/manager-api/modules/shortcuts.ts
index 83520110c131..d52747d3c853 100644
--- a/code/core/src/manager-api/modules/shortcuts.ts
+++ b/code/core/src/manager-api/modules/shortcuts.ts
@@ -382,7 +382,28 @@ export const init: ModuleFn = ({ store, fullAPI, provider }) => {
}
case 'toggleNav': {
+ const wasNavShown = fullAPI.getIsNavShown();
+ const sidebarElement = document.getElementById(focusableUIElements.sidebarRegion);
+ const wasFocusInSidebar =
+ sidebarElement &&
+ document.activeElement &&
+ sidebarElement.contains(document.activeElement);
+
fullAPI.toggleNav();
+
+ if (wasNavShown && wasFocusInSidebar) {
+ // poll: true always returns a Promise.
+ (
+ fullAPI.focusOnUIElement(focusableUIElements.showSidebar, {
+ poll: true,
+ }) as Promise
+ ).then((success) => {
+ // Fallback to body for predictable behavior.
+ if (success === false) {
+ document.body.focus();
+ }
+ });
+ }
break;
}
diff --git a/code/core/src/manager/components/layout/Layout.tsx b/code/core/src/manager/components/layout/Layout.tsx
index 310fa560ebf4..69780fef5cc3 100644
--- a/code/core/src/manager/components/layout/Layout.tsx
+++ b/code/core/src/manager/components/layout/Layout.tsx
@@ -9,10 +9,10 @@ import { styled } from 'storybook/theming';
import { MEDIA_DESKTOP_BREAKPOINT } from '../../constants';
import { Notifications } from '../../container/Notifications';
import { MobileNavigation } from '../mobile/navigation/MobileNavigation';
-import { Drag } from './Drag';
import { useLayout } from './LayoutProvider';
import { MainAreaContainer } from './MainAreaContainer';
import { PanelContainer } from './PanelContainer';
+import { SidebarContainer } from './SidebarContainer';
import { useDragging } from './useDragging';
import { useLandmarkIndicator } from './useLandmarkIndicator';
@@ -172,15 +172,8 @@ export const Layout = ({ managerLayoutState, setManagerLayoutState, hasTab, ...s
>
<>
{isDesktop && (
-
+
{slots.slotSidebar}
-
)}
{isMobile && (
@@ -245,10 +238,3 @@ const LayoutContainer = styled.div<{
})(),
},
}));
-
-const SidebarContainer = styled.div(({ theme }) => ({
- backgroundColor: theme.appBg,
- gridArea: 'sidebar',
- position: 'relative',
- borderRight: `1px solid ${theme.appBorderColor}`,
-}));
diff --git a/code/core/src/manager/components/layout/SidebarContainer.tsx b/code/core/src/manager/components/layout/SidebarContainer.tsx
new file mode 100644
index 000000000000..eff60f76aa0c
--- /dev/null
+++ b/code/core/src/manager/components/layout/SidebarContainer.tsx
@@ -0,0 +1,56 @@
+import React from 'react';
+
+import { styled } from 'storybook/theming';
+
+import { focusableUIElements } from '../../../manager-api/modules/layout';
+import { Drag } from './Drag';
+
+interface SidebarContainerProps {
+ children: React.ReactNode;
+ navSize: number;
+ sidebarResizerRef: React.Ref;
+}
+
+const Container = styled.div(({ theme }) => ({
+ backgroundColor: theme.appBg,
+ gridArea: 'sidebar',
+ position: 'relative',
+ borderRight: `1px solid ${theme.appBorderColor}`,
+}));
+
+const SidebarSlot = styled.div({
+ height: '100%',
+});
+
+/**
+ * Shows the sidebar and its resize drag handle. The drag handle is always rendered so users can
+ * reopen the sidebar. The sidebar is always rendered (to preserve internal state), but it's
+ * excluded from the Accessibility Object Model when effectively collapsed.
+ */
+const SidebarContainer = React.memo(function SidebarContainer(props) {
+ const { children, navSize, sidebarResizerRef } = props;
+
+ const shouldHideSidebarContent = navSize === 0;
+
+ return (
+
+ );
+});
+
+export { SidebarContainer };
diff --git a/code/core/src/manager/components/preview/tools/menu.tsx b/code/core/src/manager/components/preview/tools/menu.tsx
index 0cb0531d5705..404177ba6f57 100644
--- a/code/core/src/manager/components/preview/tools/menu.tsx
+++ b/code/core/src/manager/components/preview/tools/menu.tsx
@@ -8,10 +8,23 @@ import { MenuIcon } from '@storybook/icons';
import { Consumer, types } from 'storybook/manager-api';
import type { Combo } from 'storybook/manager-api';
+import { focusableUIElements } from '../../../../manager-api/modules/layout';
+import { useRegionFocusAnimation } from '../../layout/useLandmarkIndicator';
+
const menuMapper = ({ api, state }: Combo) => ({
isVisible: api.getIsNavShown(),
singleStory: state.singleStory,
- toggle: () => api.toggleNav(),
+ viewMode: state.viewMode,
+ showSidebar: async (animateLandmark?: (e: HTMLElement | null) => void) => {
+ api.toggleNav(true);
+ const success = await api.focusOnUIElement(focusableUIElements.sidebarRegion, {
+ forceFocus: true,
+ poll: true,
+ });
+ if (success) {
+ animateLandmark?.(document.getElementById(focusableUIElements.sidebarRegion));
+ }
+ },
});
export const menuTool: Addon_BaseType = {
@@ -20,25 +33,36 @@ export const menuTool: Addon_BaseType = {
type: types.TOOL,
// @ts-expect-error (non strict)
match: ({ viewMode }) => ['story', 'docs'].includes(viewMode),
- render: () => (
-
- {({ isVisible, toggle, singleStory }) =>
- !singleStory &&
- !isVisible && (
- <>
-
-
- >
- )
- }
-
- ),
+ render: () => {
+ const animateLandmark = useRegionFocusAnimation();
+
+ return (
+
+ {({ isVisible, showSidebar, singleStory }) =>
+ !singleStory &&
+ !isVisible && (
+ <>
+
+
+ >
+ )
+ }
+
+ );
+ },
};
From a1286097c495295d1f15dd3ca875bcdc072502ab Mon Sep 17 00:00:00 2001
From: Steve Dodier-Lazaro
Date: Mon, 9 Mar 2026 14:03:04 +0100
Subject: [PATCH 07/17] Fix element IDs for landmark animations
---
.../src/manager/components/layout/SidebarContainer.tsx | 2 +-
code/core/src/manager/components/panel/Panel.tsx | 3 ++-
code/core/src/manager/components/sidebar/Sidebar.tsx | 8 +++++++-
3 files changed, 10 insertions(+), 3 deletions(-)
diff --git a/code/core/src/manager/components/layout/SidebarContainer.tsx b/code/core/src/manager/components/layout/SidebarContainer.tsx
index eff60f76aa0c..d762fa53cb8c 100644
--- a/code/core/src/manager/components/layout/SidebarContainer.tsx
+++ b/code/core/src/manager/components/layout/SidebarContainer.tsx
@@ -33,7 +33,7 @@ const SidebarContainer = React.memo(function SidebarConta
const shouldHideSidebarContent = navSize === 0;
return (
-