diff --git a/packages/react/src/PageLayout/PageLayout.features.stories.tsx b/packages/react/src/PageLayout/PageLayout.features.stories.tsx
index fc08b95b767..5970b266d90 100644
--- a/packages/react/src/PageLayout/PageLayout.features.stories.tsx
+++ b/packages/react/src/PageLayout/PageLayout.features.stories.tsx
@@ -366,7 +366,7 @@ export const ResizablePaneWithoutPersistence: StoryFn = () => (
-
+
@@ -400,6 +400,12 @@ export const ResizablePaneWithCustomPersistence: StoryFn = () => {
useIsomorphicLayoutEffect(() => {
setCurrentWidth(getInitialWidth())
}, [])
+
+ const handleWidthChange = (width: number) => {
+ setCurrentWidth(width)
+ localStorage.setItem(key, width.toString())
+ }
+
return (
@@ -407,13 +413,9 @@ export const ResizablePaneWithCustomPersistence: StoryFn = () => {
{
- setCurrentWidth(width)
- localStorage.setItem(key, width.toString())
- },
- }}
+ resizable
+ currentWidth={currentWidth}
+ onWidthChange={handleWidthChange}
aria-label="Side pane"
>
@@ -448,6 +450,11 @@ export const ResizablePaneWithNumberWidth: StoryFn = () => {
const [currentWidth, setCurrentWidth] = React.useState(getInitialWidth)
+ const handleWidthChange = (newWidth: number) => {
+ setCurrentWidth(newWidth)
+ localStorage.setItem(key, newWidth.toString())
+ }
+
return (
@@ -455,13 +462,9 @@ export const ResizablePaneWithNumberWidth: StoryFn = () => {
{
- setCurrentWidth(newWidth)
- localStorage.setItem(key, newWidth.toString())
- },
- }}
+ resizable
+ currentWidth={currentWidth}
+ onWidthChange={handleWidthChange}
aria-label="Side pane"
>
@@ -496,6 +499,11 @@ export const ResizablePaneWithControlledWidth: StoryFn = () => {
const [currentWidth, setCurrentWidth] = React.useState(getInitialWidth)
+ const handleWidthChange = (newWidth: number) => {
+ setCurrentWidth(newWidth)
+ localStorage.setItem(key, newWidth.toString())
+ }
+
return (
@@ -503,13 +511,9 @@ export const ResizablePaneWithControlledWidth: StoryFn = () => {
{
- setCurrentWidth(newWidth)
- localStorage.setItem(key, newWidth.toString())
- },
- }}
+ resizable
+ currentWidth={currentWidth}
+ onWidthChange={handleWidthChange}
aria-label="Side pane"
>
diff --git a/packages/react/src/PageLayout/PageLayout.tsx b/packages/react/src/PageLayout/PageLayout.tsx
index 2b41576f61e..4b918da2b25 100644
--- a/packages/react/src/PageLayout/PageLayout.tsx
+++ b/packages/react/src/PageLayout/PageLayout.tsx
@@ -600,20 +600,29 @@ export type PageLayoutPaneProps = {
* - Named sizes: `'small'` | `'medium'` | `'large'`
* - Custom object: `{min: string, default: string, max: string}`
*
- * For controlled width (current value), use `resizable.width` instead.
+ * For controlled width (current value), use `currentWidth` prop instead.
*/
width?: PaneWidthValue
minWidth?: number
+ /**
+ * Current/controlled width value in pixels.
+ * When provided, this overrides the default width from the `width` prop.
+ * Use with `onWidthChange` for controlled width behavior.
+ */
+ currentWidth?: number
+ /**
+ * Callback fired when the pane width changes (during resize).
+ * Only called when `resizable` is enabled.
+ * When provided, this callback is used instead of any persistence mechanism.
+ * Use with `currentWidth` for controlled width behavior.
+ */
+ onWidthChange?: (width: number) => void
/**
* Enable resizable pane behavior.
- * - `true`: Enable with default localStorage persistence
- * - `false`: Disable resizing
- * - `{width?: number, persist: false}`: Enable without persistence, optionally with controlled current width
- * - `{width?: number, persist: 'localStorage'}`: Enable with localStorage, optionally with controlled current width
- * - `{width?: number, persist: fn}`: Enable with custom persistence, optionally with controlled current width
+ * - `true`: Enable resizing with localStorage persistence (only if onWidthChange is not provided)
+ * - `false` or `undefined`: Disable resizing
*
- * The `width` property in the config represents the current/controlled width value.
- * When provided, it takes precedence over the default width from the `width` prop.
+ * Note: When `onWidthChange` is provided, it takes precedence over localStorage persistence.
*/
resizable?: ResizableConfig
widthStorageKey?: string
@@ -659,6 +668,8 @@ const Pane = React.forwardRef {
expect(result.current.currentWidth).toBe(defaultPaneWidth.medium)
})
- it('should not read from localStorage when {persist: false} is provided', () => {
+ it('should not read from localStorage when onWidthChange is provided', () => {
localStorage.setItem('test-pane', '500')
const refs = createMockRefs()
+ const onWidthChange = vi.fn()
const {result} = renderHook(() =>
usePaneWidth({
width: 'medium',
minWidth: 256,
- resizable: {persist: false},
+ resizable: true,
widthStorageKey: 'test-pane',
+ onWidthChange,
...refs,
}),
)
- // Should use default, not localStorage value
+ // Should use default, not localStorage value when onWidthChange is provided
expect(result.current.currentWidth).toBe(defaultPaneWidth.medium)
})
- it('should not save to any storage when {persist: false} is provided', () => {
+ it('should not save to localStorage when onWidthChange is provided', () => {
const refs = createMockRefs()
+ const onWidthChange = vi.fn()
const {result} = renderHook(() =>
usePaneWidth({
width: 'medium',
minWidth: 256,
- resizable: {persist: false},
+ resizable: true,
widthStorageKey: 'test-pane',
+ onWidthChange,
...refs,
}),
)
@@ -172,27 +171,30 @@ describe('usePaneWidth', () => {
// Width state should update
expect(result.current.currentWidth).toBe(450)
+ // onWidthChange should be called
+ expect(onWidthChange).toHaveBeenCalledWith(450)
// But localStorage should not be written
expect(localStorage.getItem('test-pane')).toBeNull()
})
- it('should initialize with resizable.width when provided', () => {
+ it('should initialize with currentWidth prop when provided', () => {
const refs = createMockRefs()
const {result} = renderHook(() =>
usePaneWidth({
width: 'medium',
minWidth: 256,
- resizable: {width: 400, persist: false},
+ resizable: true,
widthStorageKey: 'test-pane',
+ currentWidth: 400,
...refs,
}),
)
- // Should use resizable.width, not the default from width prop
+ // Should use currentWidth prop, not the default from width prop
expect(result.current.currentWidth).toBe(400)
})
- it('should prefer resizable.width over localStorage', () => {
+ it('should prefer currentWidth prop over localStorage', () => {
localStorage.setItem('test-pane', '350')
const refs = createMockRefs()
@@ -200,83 +202,85 @@ describe('usePaneWidth', () => {
usePaneWidth({
width: 'medium',
minWidth: 256,
- resizable: {width: 500, persist: 'localStorage'},
+ resizable: true,
widthStorageKey: 'test-pane',
+ currentWidth: 500,
...refs,
}),
)
- // Should use resizable.width, not localStorage
+ // Should use currentWidth prop, not localStorage
expect(result.current.currentWidth).toBe(500)
})
- it('should sync when resizable.width changes', () => {
+ it('should sync when currentWidth prop changes', () => {
const refs = createMockRefs()
- type ResizableType = {width?: number; persist: false}
const {result, rerender} = renderHook(
- ({resizable}: {resizable: ResizableType}) =>
+ ({currentWidth}: {currentWidth?: number}) =>
usePaneWidth({
width: 'medium',
minWidth: 256,
- resizable,
+ resizable: true,
widthStorageKey: 'test-sync-resizable',
+ currentWidth,
...refs,
}),
- {initialProps: {resizable: {width: 350, persist: false} as ResizableType}},
+ {initialProps: {currentWidth: 350}},
)
expect(result.current.currentWidth).toBe(350)
- // Change resizable.width
- rerender({resizable: {width: 450, persist: false}})
+ // Change currentWidth prop
+ rerender({currentWidth: 450})
expect(result.current.currentWidth).toBe(450)
})
- it('should fall back to default when resizable.width is removed', () => {
+ it('should fall back to default when currentWidth prop is removed', () => {
const refs = createMockRefs()
- type ResizableType = {width?: number; persist: false}
+ type Props = {currentWidth?: number}
const {result, rerender} = renderHook(
- ({resizable}: {resizable: ResizableType}) =>
+ ({currentWidth}: Props) =>
usePaneWidth({
width: 'medium',
minWidth: 256,
- resizable,
+ resizable: true,
widthStorageKey: 'test-fallback',
+ currentWidth,
...refs,
}),
- {initialProps: {resizable: {width: 400, persist: false} as ResizableType}},
+ {initialProps: {currentWidth: 400} as Props},
)
expect(result.current.currentWidth).toBe(400)
- // Remove width from resizable config
- rerender({resizable: {persist: false}})
+ // Remove currentWidth prop by not passing it
+ rerender({} as Props)
// Should fall back to default from width prop
expect(result.current.currentWidth).toBe(defaultPaneWidth.medium)
})
- it('should not sync width prop default when resizable.width is provided', () => {
+ it('should not sync width prop default when currentWidth prop is provided', () => {
const refs = createMockRefs()
type WidthType = 'small' | 'medium' | 'large'
- type ResizableType = {width: number; persist: false}
const {result, rerender} = renderHook(
- ({width, resizable}: {width: WidthType; resizable: ResizableType}) =>
+ ({width, currentWidth}: {width: WidthType; currentWidth: number}) =>
usePaneWidth({
width,
minWidth: 256,
- resizable,
+ resizable: true,
widthStorageKey: 'test-no-sync',
+ currentWidth,
...refs,
}),
{
initialProps: {
width: 'medium' as WidthType,
- resizable: {width: 400, persist: false} as ResizableType,
+ currentWidth: 400,
},
},
)
@@ -284,9 +288,9 @@ describe('usePaneWidth', () => {
expect(result.current.currentWidth).toBe(400)
// Change width prop (default changes from 296 to 320)
- rerender({width: 'large', resizable: {width: 400, persist: false}})
+ rerender({width: 'large', currentWidth: 400})
- // Should NOT sync to new default because resizable.width is controlling
+ // Should NOT sync to new default because currentWidth prop is controlling
expect(result.current.currentWidth).toBe(400)
})
})
@@ -343,37 +347,17 @@ describe('usePaneWidth', () => {
localStorage.setItem = originalSetItem
})
- it('should use localStorage when {persist: "localStorage"} is provided', () => {
- const refs = createMockRefs()
- const {result} = renderHook(() =>
- usePaneWidth({
- width: 'medium',
- minWidth: 256,
- resizable: {persist: 'localStorage'},
- widthStorageKey: 'test-explicit-localstorage',
- ...refs,
- }),
- )
-
- act(() => {
- result.current.saveWidth(450)
- })
-
- expect(result.current.currentWidth).toBe(450)
- expect(localStorage.getItem('test-explicit-localstorage')).toBe('450')
- })
-
- it('should call custom save function with width and options', () => {
- const customSave = vi.fn()
- const customPersister: PersistConfig = {persist: customSave}
+ it('should call onWidthChange instead of localStorage when provided', () => {
+ const onWidthChange = vi.fn()
const refs = createMockRefs()
const {result} = renderHook(() =>
usePaneWidth({
width: 'medium',
minWidth: 256,
- resizable: customPersister,
- widthStorageKey: 'my-custom-key',
+ resizable: true,
+ widthStorageKey: 'test-onWidthChange',
+ onWidthChange,
...refs,
}),
)
@@ -383,71 +367,24 @@ describe('usePaneWidth', () => {
})
expect(result.current.currentWidth).toBe(450)
- expect(customSave).toHaveBeenCalledWith(450, {widthStorageKey: 'my-custom-key'})
- // Should NOT write to localStorage
- expect(localStorage.getItem('my-custom-key')).toBeNull()
+ expect(onWidthChange).toHaveBeenCalledWith(450)
+ // Should NOT write to localStorage when onWidthChange is provided
+ expect(localStorage.getItem('test-onWidthChange')).toBeNull()
})
- it('should handle async custom save function', async () => {
- const customSave = vi.fn().mockResolvedValue(undefined)
- const customPersister: PersistConfig = {persist: customSave}
- const refs = createMockRefs()
-
- const {result} = renderHook(() =>
- usePaneWidth({
- width: 'medium',
- minWidth: 256,
- resizable: customPersister,
- widthStorageKey: 'test-async',
- ...refs,
- }),
- )
-
- act(() => {
- result.current.saveWidth(350)
- })
-
- expect(result.current.currentWidth).toBe(350)
- expect(customSave).toHaveBeenCalledWith(350, {widthStorageKey: 'test-async'})
- })
-
- it('should handle sync errors from custom save gracefully', () => {
- const customSave = vi.fn(() => {
- throw new Error('Sync storage error')
- })
- const customPersister: PersistConfig = {persist: customSave}
- const refs = createMockRefs()
-
- const {result} = renderHook(() =>
- usePaneWidth({
- width: 'medium',
- minWidth: 256,
- resizable: customPersister,
- widthStorageKey: 'test-sync-error',
- ...refs,
- }),
- )
-
- // Should not throw - state should still update
- act(() => {
- result.current.saveWidth(450)
+ it('should handle errors from onWidthChange gracefully', () => {
+ const onWidthChange = vi.fn(() => {
+ throw new Error('Consumer callback error')
})
-
- expect(result.current.currentWidth).toBe(450)
- expect(customSave).toHaveBeenCalledWith(450, {widthStorageKey: 'test-sync-error'})
- })
-
- it('should handle async rejection from custom save gracefully', async () => {
- const customSave = vi.fn().mockRejectedValue(new Error('Async storage error'))
- const customPersister: PersistConfig = {persist: customSave}
const refs = createMockRefs()
const {result} = renderHook(() =>
usePaneWidth({
width: 'medium',
minWidth: 256,
- resizable: customPersister,
- widthStorageKey: 'test-async-error',
+ resizable: true,
+ widthStorageKey: 'test-onWidthChange-error',
+ onWidthChange,
...refs,
}),
)
@@ -457,31 +394,8 @@ describe('usePaneWidth', () => {
result.current.saveWidth(450)
})
- // Wait for promise rejection to be handled
- await vi.waitFor(() => {
- expect(customSave).toHaveBeenCalledWith(450, {widthStorageKey: 'test-async-error'})
- })
-
expect(result.current.currentWidth).toBe(450)
- })
-
- it('should not read from localStorage when custom save is provided', () => {
- localStorage.setItem('test-pane', '500')
- const customPersister: PersistConfig = {persist: vi.fn() as PersistFunction}
- const refs = createMockRefs()
-
- const {result} = renderHook(() =>
- usePaneWidth({
- width: 'medium',
- minWidth: 256,
- resizable: customPersister,
- widthStorageKey: 'test-pane',
- ...refs,
- }),
- )
-
- // Should use default, not localStorage value
- expect(result.current.currentWidth).toBe(defaultPaneWidth.medium)
+ expect(onWidthChange).toHaveBeenCalledWith(450)
})
})
@@ -1196,73 +1110,4 @@ describe('constants', () => {
})
})
-describe('type guards', () => {
- describe('isResizableEnabled', () => {
- it('should return true for boolean true', () => {
- expect(isResizableEnabled(true)).toBe(true)
- })
-
- it('should return false for boolean false', () => {
- expect(isResizableEnabled(false)).toBe(false)
- })
-
- it('should return true for {persist: false} (resizable without persistence)', () => {
- expect(isResizableEnabled({persist: false})).toBe(true)
- })
- it('should return true for {persist: "localStorage"}', () => {
- expect(isResizableEnabled({persist: 'localStorage'})).toBe(true)
- })
-
- it('should return true for {persist: fn} (custom persistence)', () => {
- expect(isResizableEnabled({persist: () => {}})).toBe(true)
- })
- })
-
- describe('isPersistConfig', () => {
- it('should return true for {persist: false}', () => {
- expect(isPersistConfig({persist: false})).toBe(true)
- })
-
- it('should return true for {persist: "localStorage"}', () => {
- expect(isPersistConfig({persist: 'localStorage'})).toBe(true)
- })
-
- it('should return true for {persist: fn}', () => {
- expect(isPersistConfig({persist: () => {}})).toBe(true)
- })
-
- it('should return false for boolean true', () => {
- expect(isPersistConfig(true)).toBe(false)
- })
-
- it('should return false for boolean false', () => {
- expect(isPersistConfig(false)).toBe(false)
- })
-
- it('should return false for objects without persist property', () => {
- // @ts-expect-error - testing runtime behavior with arbitrary object
- expect(isPersistConfig({other: 'value'})).toBe(false)
- })
- })
-
- describe('isCustomPersistFunction', () => {
- it('should return true for function', () => {
- const fn = () => {}
- expect(isCustomPersistFunction(fn)).toBe(true)
- })
-
- it('should return true for async function', () => {
- const fn = async () => {}
- expect(isCustomPersistFunction(fn)).toBe(true)
- })
-
- it('should return false for false', () => {
- expect(isCustomPersistFunction(false)).toBe(false)
- })
-
- it('should return false for "localStorage"', () => {
- expect(isCustomPersistFunction('localStorage')).toBe(false)
- })
- })
-})
diff --git a/packages/react/src/PageLayout/usePaneWidth.ts b/packages/react/src/PageLayout/usePaneWidth.ts
index 392ec91e9de..262b85a912d 100644
--- a/packages/react/src/PageLayout/usePaneWidth.ts
+++ b/packages/react/src/PageLayout/usePaneWidth.ts
@@ -23,49 +23,11 @@ export type PaneWidth = 'small' | 'medium' | 'large'
export type PaneWidthValue = PaneWidth | CustomWidthOptions
/**
- * Options passed to custom persist function.
+ * Resizable configuration.
+ * - `true`: Enable resizing (uses localStorage by default if onWidthChange not provided)
+ * - `false` or `undefined`: Disable resizing
*/
-export type SaveOptions = {widthStorageKey: string}
-
-/**
- * Custom persist function type.
- */
-export type PersistFunction = (width: number, options: SaveOptions) => void | Promise
-
-/**
- * Configuration object for resizable pane.
- * - `width?: number` - Current/controlled width value in pixels (overrides width prop's default)
- * - `persist: false` - Enable resizing without any persistence
- * - `persist: 'localStorage'` - Enable resizing with localStorage persistence
- * - `persist: fn` - Enable resizing with custom persistence function
- */
-export type PersistConfig = {
- /** Current controlled width value in pixels. When provided, this overrides the default from the width prop. */
- width?: number
- persist: false | 'localStorage' | PersistFunction
-}
-
-/**
- * Type guard to check if persist value is a custom function
- */
-export const isCustomPersistFunction = (
- persist: false | 'localStorage' | PersistFunction,
-): persist is PersistFunction => {
- return typeof persist === 'function'
-}
-
-/**
- * Resizable configuration options.
- * - `true`: Enable resizing with default localStorage persistence (may cause hydration mismatch)
- * - `false`: Disable resizing
- * - `{width?: number, persist: false}`: Enable resizing without any persistence, optionally with controlled width
- * - `{width?: number, persist: 'localStorage'}`: Enable resizing with localStorage persistence, optionally with controlled width
- * - `{width?: number, persist: fn}`: Enable resizing with custom persistence function, optionally with controlled width
- *
- * The `width` property in the config object represents the current/controlled width value.
- * When provided, it takes precedence over the default width from the `width` prop.
- */
-export type ResizableConfig = boolean | PersistConfig
+export type ResizableConfig = boolean | undefined
export type UsePaneWidthOptions = {
width: PaneWidthValue
@@ -75,6 +37,10 @@ export type UsePaneWidthOptions = {
paneRef: React.RefObject
handleRef: React.RefObject
contentWrapperRef: React.RefObject
+ /** Callback to notify of width changes when resizable */
+ onWidthChange?: (width: number) => void
+ /** Current/controlled width value (overrides default from width prop) */
+ currentWidth?: number
}
export type UsePaneWidthResult = {
@@ -140,21 +106,6 @@ export const getDefaultPaneWidth = (w: PaneWidthValue): number => {
return 0
}
-/**
- * Type guard to check if resizable config is a PersistConfig object
- */
-export const isPersistConfig = (config: ResizableConfig): config is PersistConfig => {
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- config could be null at runtime despite types
- return typeof config === 'object' && config !== null && 'persist' in config
-}
-
-/**
- * Check if resizing is enabled (boolean true or {persist: ...})
- */
-export const isResizableEnabled = (config: ResizableConfig): boolean => {
- return config === true || isPersistConfig(config)
-}
-
/**
* Gets the --pane-max-width-diff CSS variable value from a pane element.
* This value is set by CSS media queries and controls the max pane width constraint.
@@ -214,9 +165,9 @@ const localStoragePersister = {
* functions to save and reset width.
*
* Storage behavior:
- * - When `resizable` is `true`: Uses localStorage with the provided `widthStorageKey`
- * - When `resizable` is `{persist: false}`: Resizable without any persistence
- * - When `resizable` is `{save: fn}`: Resizable with custom persistence
+ * - When `resizable` is `true` and `onWidthChange` is not provided: Uses localStorage
+ * - When `onWidthChange` is provided: Calls the callback instead of localStorage
+ * - When `resizable` is `false` or `undefined`: Not resizable, no persistence
*/
export function usePaneWidth({
width,
@@ -226,6 +177,8 @@ export function usePaneWidth({
paneRef,
handleRef,
contentWrapperRef,
+ onWidthChange,
+ currentWidth: controlledWidth,
}: UsePaneWidthOptions): UsePaneWidthResult {
// Derive constraints from width configuration
const isCustomWidth = isCustomWidthOptions(width)
@@ -235,11 +188,13 @@ export function usePaneWidth({
// Refs for stable callbacks - updated in layout effect below
const widthStorageKeyRef = React.useRef(widthStorageKey)
const resizableRef = React.useRef(resizable)
+ const onWidthChangeRef = React.useRef(onWidthChange)
// Keep refs in sync with props for stable callbacks
useIsomorphicLayoutEffect(() => {
resizableRef.current = resizable
widthStorageKeyRef.current = widthStorageKey
+ onWidthChangeRef.current = onWidthChange
})
// Cache the CSS variable value to avoid getComputedStyle during drag (causes layout thrashing)
// Updated on mount and resize when breakpoints might change
@@ -256,17 +211,18 @@ export function usePaneWidth({
// --- State ---
// Current width for React renders (ARIA attributes). Updates go through saveWidth() or clamp on resize.
// Priority order for initial width:
- // 1. resizable.width (controlled current value)
- // 2. localStorage (resizable === true only)
+ // 1. currentWidth prop (controlled current value)
+ // 2. localStorage (if onWidthChange is not provided and resizable is true)
// 3. defaultWidth (from width prop)
- const [currentWidth, setCurrentWidth] = React.useState(() => {
- // Check if resizable config has a controlled width value
- if (isPersistConfig(resizable) && typeof resizable.width === 'number') {
- return resizable.width
+ const [currentWidthState, setCurrentWidthState] = React.useState(() => {
+ // Check if controlled width value is provided
+ if (typeof controlledWidth === 'number') {
+ return controlledWidth
}
- // Only try localStorage for default persister (resizable === true)
+ // Try localStorage if onWidthChange is not provided (default persistence behavior)
// Read directly here instead of via persister to satisfy react-hooks/refs lint rule
- if (resizable === true) {
+ const shouldUseLocalStorage = onWidthChange === undefined && resizable === true
+ if (shouldUseLocalStorage) {
const storedWidth = localStoragePersister.get(widthStorageKey)
if (storedWidth !== null) {
return storedWidth
@@ -275,10 +231,9 @@ export function usePaneWidth({
return defaultWidth
})
- // Inline state sync when width prop or resizable.width changes (avoids effect)
+ // Inline state sync when width prop or controlled width changes (avoids effect)
// See: https://react.dev/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes
const [prevDefaultWidth, setPrevDefaultWidth] = React.useState(defaultWidth)
- const controlledWidth = isPersistConfig(resizable) ? resizable.width : undefined
const [prevControlledWidth, setPrevControlledWidth] = React.useState(controlledWidth)
// Handle controlled width changes
@@ -289,21 +244,24 @@ export function usePaneWidth({
setPrevControlledWidth(controlledWidth)
if (typeof controlledWidth === 'number') {
// New controlled width provided
- setCurrentWidth(controlledWidth)
+ setCurrentWidthState(controlledWidth)
} else if (prevControlledWidth !== undefined) {
// Controlled width was removed, fall back to default
- setCurrentWidth(defaultWidth)
+ setCurrentWidthState(defaultWidth)
}
}
if (defaultWidthChanged) {
setPrevDefaultWidth(defaultWidth)
- // Only sync defaultWidth to currentWidth if there's no controlled width
+ // Only sync defaultWidth to currentWidthState if there's no controlled width
if (controlledWidth === undefined && !controlledWidthChanged) {
- setCurrentWidth(defaultWidth)
+ setCurrentWidthState(defaultWidth)
}
}
+ // Use controlled width if provided, otherwise use internal state
+ const currentWidth = controlledWidth ?? currentWidthState
+
// Mutable ref for drag operations - avoids re-renders on every pixel move
const currentWidthRef = React.useRef(currentWidth)
// Max width for ARIA - SSR uses custom max or a sensible default, updated on mount
@@ -321,27 +279,24 @@ export function usePaneWidth({
currentWidthRef.current = value
// Visual update already done via inline styles - React state sync is non-urgent
startTransition(() => {
- setCurrentWidth(value)
+ setCurrentWidthState(value)
})
- const config = resizableRef.current
-
- // Handle localStorage persistence: resizable === true or {persist: 'localStorage'}
- if (config === true || (isPersistConfig(config) && config.persist === 'localStorage')) {
- localStoragePersister.save(widthStorageKeyRef.current, value)
- } else if (isPersistConfig(config) && isCustomPersistFunction(config.persist)) {
+ // If onWidthChange is provided, call it instead of any persistence
+ if (onWidthChangeRef.current) {
try {
- const result = config.persist(value, {widthStorageKey: widthStorageKeyRef.current})
- // Handle async rejections silently
- if (result instanceof Promise) {
- // eslint-disable-next-line github/no-then
- result.catch(() => {
- // Ignore - consumer should handle their own errors
- })
- }
+ onWidthChangeRef.current(value)
} catch {
- // Ignore sync errors
+ // Ignore errors from consumer callback
}
+ return
+ }
+
+ const config = resizableRef.current
+
+ // Handle localStorage persistence when resizable === true
+ if (config === true) {
+ localStoragePersister.save(widthStorageKeyRef.current, value)
}
}, [])
@@ -355,7 +310,7 @@ export function usePaneWidth({
// Update CSS variable, refs, and ARIA on mount and window resize.
// Strategy: Only sync when resize stops (debounced) to avoid layout thrashing on large DOMs
useIsomorphicLayoutEffect(() => {
- if (!isResizableEnabled(resizableRef.current)) return
+ if (!resizableRef.current) return
let lastViewportWidth = window.innerWidth
@@ -394,7 +349,7 @@ export function usePaneWidth({
startTransition(() => {
setMaxPaneWidth(actualMax)
if (wasClamped) {
- setCurrentWidth(actualMax)
+ setCurrentWidthState(actualMax)
}
})
}