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) } }) }