diff --git a/change/@fluentui-react-drawer-4303150f-5c8d-4bf5-a0bc-e7e9d2f3fcb1.json b/change/@fluentui-react-drawer-4303150f-5c8d-4bf5-a0bc-e7e9d2f3fcb1.json new file mode 100644 index 00000000000000..39de0ab3187ebd --- /dev/null +++ b/change/@fluentui-react-drawer-4303150f-5c8d-4bf5-a0bc-e7e9d2f3fcb1.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "fix: update drawer scroll state when content changes", + "packageName": "@fluentui/react-drawer", + "email": "marcosvmmoura@gmail.com", + "dependentChangeType": "patch" +} diff --git a/packages/react-components/react-drawer/library/config/tests.js b/packages/react-components/react-drawer/library/config/tests.js index 2e211ae9e21420..67640889fffb15 100644 --- a/packages/react-components/react-drawer/library/config/tests.js +++ b/packages/react-components/react-drawer/library/config/tests.js @@ -1 +1,24 @@ /** Jest test setup file. */ + +/** + * Mock ResizeObserver for test environment + * + * Required because DrawerBody component depends on ResizeObserver, but jsdom doesn't provide this browser API + */ +global.ResizeObserver = class ResizeObserver { + constructor(callback) { + this.callback = callback; + } + + observe() { + // Do nothing in tests - we're only testing component behavior, not resize functionality + } + + unobserve() { + // Do nothing in tests + } + + disconnect() { + // Do nothing in tests + } +}; diff --git a/packages/react-components/react-drawer/library/src/components/DrawerBody/DrawerBody.cy.tsx b/packages/react-components/react-drawer/library/src/components/DrawerBody/DrawerBody.cy.tsx index 6ba117647116c1..eabf044228da6c 100644 --- a/packages/react-components/react-drawer/library/src/components/DrawerBody/DrawerBody.cy.tsx +++ b/packages/react-components/react-drawer/library/src/components/DrawerBody/DrawerBody.cy.tsx @@ -5,6 +5,7 @@ import { webLightTheme } from '@fluentui/react-theme'; import { DrawerBody } from './DrawerBody'; import type { JSXElement } from '@fluentui/react-utilities'; +import { DrawerProvider, useDrawerContextValue } from '../../contexts'; const mountFluent = (element: JSXElement) => { mount({element}); @@ -39,4 +40,48 @@ describe('DrawerBody', () => { .scrollTo('top') .should($e => assertScrollPosition($e[0], 0)); }); + + it('updates scrollState when children change from short to long', () => { + const shortContent = 'Short content'; + const longContent = Array(50) + .fill( + 'lorem ipsum dolor sit amet consectetur, adipisicing elit. Corrupti, animi? Quos, eum pariatur. Labore magni vel doloremque reiciendis, consequatur porro explicabo similique harum illo, ad hic, earum nobis accusantium quasi?', + ) + .join(' '); + + const Example = () => { + const context = useDrawerContextValue(); + const [showLong, setShowLong] = React.useState(false); + + return ( + +
{context.scrollState}
+ + + {showLong ? longContent : shortContent} + + + +
+ ); + }; + + mountFluent(); + + // Initially short content should result in 'none' + cy.get('#drawer-body').should('exist'); + cy.get('#scroll-state').should('have.text', 'none'); + + // Toggle to long content and assert context scroll state updates to 'top' + cy.get('#toggle-content').click(); + cy.get('#scroll-state').should('have.text', 'top'); + + // Scroll to bottom and assert it becomes 'bottom' + cy.get('#drawer-body') + .scrollTo('bottom') + // wait for any rAF-based updates and then assert the scrollState + .then(() => cy.get('#scroll-state').should('have.text', 'bottom')); + }); }); diff --git a/packages/react-components/react-drawer/library/src/components/DrawerBody/useDrawerBody.ts b/packages/react-components/react-drawer/library/src/components/DrawerBody/useDrawerBody.ts index b7b16ee71aa49d..7e34a9ca0aa5f5 100644 --- a/packages/react-components/react-drawer/library/src/components/DrawerBody/useDrawerBody.ts +++ b/packages/react-components/react-drawer/library/src/components/DrawerBody/useDrawerBody.ts @@ -14,6 +14,7 @@ import { useDrawerContext_unstable } from '../../contexts/drawerContext'; import { DrawerScrollState } from '../../shared/DrawerBase.types'; import type { DrawerBodyProps, DrawerBodyState } from './DrawerBody.types'; +import { useFluent_unstable as useFluent } from '@fluentui/react-shared-contexts'; /** * @internal @@ -48,10 +49,16 @@ const getScrollState = ({ scrollTop, scrollHeight, clientHeight }: HTMLElement): * @param ref - reference to root HTMLElement of DrawerBody */ export const useDrawerBody_unstable = (props: DrawerBodyProps, ref: React.Ref): DrawerBodyState => { + const { targetDocument } = useFluent(); + const win = targetDocument?.defaultView; + const { setScrollState } = useDrawerContext_unstable(); const scrollRef = React.useRef(null); - const [setAnimationFrame, cancelAnimationFrame] = useAnimationFrame(); + const mergedRef = useMergedRefs(ref, scrollRef); + + const [setScrollAnimationFrame, cancelScrollAnimationFrame] = useAnimationFrame(); + const [setResizeAnimationFrame, cancelResizeAnimationFrame] = useAnimationFrame(); const updateScrollState = React.useCallback(() => { if (!scrollRef.current) { @@ -62,23 +69,28 @@ export const useDrawerBody_unstable = (props: DrawerBodyProps, ref: React.Ref { - cancelAnimationFrame(); - setAnimationFrame(() => updateScrollState()); - }, [cancelAnimationFrame, setAnimationFrame, updateScrollState]); + cancelScrollAnimationFrame(); + setScrollAnimationFrame(updateScrollState); + }, [cancelScrollAnimationFrame, setScrollAnimationFrame, updateScrollState]); - useIsomorphicLayoutEffect(() => { - cancelAnimationFrame(); - setAnimationFrame(() => updateScrollState()); - /* update scroll state when children changes */ - return () => cancelAnimationFrame(); - }, [props.children, cancelAnimationFrame, updateScrollState, setAnimationFrame]); + // Update scroll state on children change + useIsomorphicLayoutEffect(updateScrollState, [props.children, updateScrollState]); + // Update scroll state on mount and when resize occurs useIsomorphicLayoutEffect(() => { - cancelAnimationFrame(); - setAnimationFrame(() => updateScrollState()); + if (!scrollRef.current || !win?.ResizeObserver) { + return; + } + + const observer = new win.ResizeObserver(() => setResizeAnimationFrame(updateScrollState)); + + observer.observe(scrollRef.current); - return () => cancelAnimationFrame(); - }, [cancelAnimationFrame, updateScrollState, setAnimationFrame]); + return () => { + observer.disconnect(); + cancelResizeAnimationFrame(); + }; + }, [setResizeAnimationFrame, cancelResizeAnimationFrame, updateScrollState, win]); return { components: { @@ -87,10 +99,7 @@ export const useDrawerBody_unstable = (props: DrawerBodyProps, ref: React.Ref('div', { - // FIXME: - // `ref` is wrongly assigned to be `HTMLElement` instead of `HTMLDivElement` - // but since it would be a breaking change to fix it, we are casting ref to it's proper type - ref: useMergedRefs(ref as React.Ref, scrollRef), + ref: mergedRef, ...props, onScroll: mergeCallbacks(props.onScroll, onScroll), }),