From 65f7a65576e2d03d462d4d02310c71886acdba3d Mon Sep 17 00:00:00 2001 From: aznamenacek Date: Tue, 23 Jun 2026 10:28:53 +0200 Subject: [PATCH 1/5] fix(react-drawer): tolerate fractional scroll metrics at bottom --- .../library/src/components/DrawerBody/useDrawerBody.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 881fc003aa006..7b5c56f486597 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 @@ -16,6 +16,10 @@ import type { DrawerScrollState } from '../../shared/DrawerBase.types'; import type { DrawerBodyProps, DrawerBodyState } from './DrawerBody.types'; import { useFluent_unstable as useFluent } from '@fluentui/react-shared-contexts'; +// Allow a 1px tolerance so fractional scroll metrics (browser zoom, display scaling) +// still resolve to 'bottom' instead of getting stuck at 'middle' +const SCROLL_BOTTOM_TOLERANCE = 1; + /** * Get the current scroll state of the DrawerBody. * @@ -31,7 +35,7 @@ const getScrollState = ({ scrollTop, scrollHeight, clientHeight }: HTMLElement): return 'top'; } - if (scrollTop + clientHeight === scrollHeight) { + if (scrollTop + clientHeight >= scrollHeight - SCROLL_BOTTOM_TOLERANCE) { return 'bottom'; } From dbd0b95e2e02f56c6f87e13015f30a50682a1723 Mon Sep 17 00:00:00 2001 From: aznamenacek Date: Tue, 23 Jun 2026 10:31:52 +0200 Subject: [PATCH 2/5] chore: add change file for react-drawer scroll fix --- ...-react-drawer-dcd47fc8-4a10-4374-b279-78f83587474c.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 change/@fluentui-react-drawer-dcd47fc8-4a10-4374-b279-78f83587474c.json diff --git a/change/@fluentui-react-drawer-dcd47fc8-4a10-4374-b279-78f83587474c.json b/change/@fluentui-react-drawer-dcd47fc8-4a10-4374-b279-78f83587474c.json new file mode 100644 index 0000000000000..5f937c634bb70 --- /dev/null +++ b/change/@fluentui-react-drawer-dcd47fc8-4a10-4374-b279-78f83587474c.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "fix(react-drawer): allow 1px tolerance in scroll-bottom detection so the footer divider hides at >100% zoom", + "packageName": "@fluentui/react-drawer", + "email": "adam.znamenacek@eway-crm.com", + "dependentChangeType": "patch" +} From a3a0adbe2d37e365c4c8a11e86ca46e21e648ee4 Mon Sep 17 00:00:00 2001 From: PaulGMardling Date: Wed, 8 Jul 2026 10:44:42 +0200 Subject: [PATCH 3/5] fix(react-drawer): handle fractional bottom scroll and add regression tests --- .../components/DrawerBody/DrawerBody.cy.tsx | 45 +++++++++++++ .../components/DrawerBody/DrawerBody.test.tsx | 63 +++++++++++++++++++ .../components/DrawerBody/useDrawerBody.ts | 2 +- 3 files changed, 109 insertions(+), 1 deletion(-) 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 eabf044228da6..9de3ced6680db 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 @@ -84,4 +84,49 @@ describe('DrawerBody', () => { // wait for any rAF-based updates and then assert the scrollState .then(() => cy.get('#scroll-state').should('have.text', 'bottom')); }); + + it('treats near-bottom fractional scroll values as bottom', () => { + const Example = () => { + const context = useDrawerContextValue(); + + return ( + +
{context.scrollState}
+ + Content + +
+ ); + }; + + mountFluent(); + + cy.get('#drawer-body').then($e => { + const element = $e[0] as HTMLDivElement; + let mockedScrollTop = 89.4; + + Object.defineProperty(element, 'clientHeight', { + value: 100, + configurable: true, + }); + + Object.defineProperty(element, 'scrollHeight', { + value: 190, + configurable: true, + }); + + Object.defineProperty(element, 'scrollTop', { + configurable: true, + get: () => mockedScrollTop, + set: value => { + mockedScrollTop = value; + }, + }); + + mockedScrollTop = 89.4; + element.dispatchEvent(new Event('scroll', { bubbles: true })); + }); + + cy.get('#scroll-state').should('have.text', 'bottom'); + }); }); diff --git a/packages/react-components/react-drawer/library/src/components/DrawerBody/DrawerBody.test.tsx b/packages/react-components/react-drawer/library/src/components/DrawerBody/DrawerBody.test.tsx index 5b2a912d41d32..6d44d0e8c0efe 100644 --- a/packages/react-components/react-drawer/library/src/components/DrawerBody/DrawerBody.test.tsx +++ b/packages/react-components/react-drawer/library/src/components/DrawerBody/DrawerBody.test.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import { render } from '@testing-library/react'; import { DrawerBody } from './DrawerBody'; +import { getScrollState } from './useDrawerBody'; import { isConformant } from '../../testing/isConformant'; describe('DrawerBody', () => { @@ -21,4 +22,66 @@ describe('DrawerBody', () => { `); }); + + describe('getScrollState', () => { + it('returns none when content does not overflow', () => { + const state = getScrollState({ + scrollTop: 0, + scrollHeight: 100, + clientHeight: 100, + } as HTMLElement); + + expect(state).toBe('none'); + }); + + it('returns top when at the start of scrollable content', () => { + const state = getScrollState({ + scrollTop: 0, + scrollHeight: 200, + clientHeight: 100, + } as HTMLElement); + + expect(state).toBe('top'); + }); + + it('returns middle when between top and bottom', () => { + const state = getScrollState({ + scrollTop: 50, + scrollHeight: 200, + clientHeight: 100, + } as HTMLElement); + + expect(state).toBe('middle'); + }); + + it('returns bottom when at the end of scrollable content', () => { + const state = getScrollState({ + scrollTop: 100, + scrollHeight: 200, + clientHeight: 100, + } as HTMLElement); + + expect(state).toBe('bottom'); + }); + + it('returns bottom when within 1px tolerance at the end', () => { + const state = getScrollState({ + scrollTop: 89.4, + scrollHeight: 190, + clientHeight: 100, + } as HTMLElement); + + expect(state).toBe('bottom'); + }); + + it('returns middle when more than 1px away from the end', () => { + const state = getScrollState({ + scrollTop: 88.9, + scrollHeight: 190, + clientHeight: 100, + } as HTMLElement); + + expect(state).toBe('middle'); + }); + }); }); 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 7b5c56f486597..a8a6e895cb62b 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 @@ -26,7 +26,7 @@ const SCROLL_BOTTOM_TOLERANCE = 1; * @internal * @param element - HTMLElement to check scroll state of */ -const getScrollState = ({ scrollTop, scrollHeight, clientHeight }: HTMLElement): DrawerScrollState => { +export const getScrollState = ({ scrollTop, scrollHeight, clientHeight }: HTMLElement): DrawerScrollState => { if (scrollHeight <= clientHeight) { return 'none'; } From 021f7be9d22aefb8bc5b88641ddf02a4e2c34b8d Mon Sep 17 00:00:00 2001 From: PaulGMardling Date: Wed, 8 Jul 2026 12:20:05 +0200 Subject: [PATCH 4/5] fix(react-drawer): remove redundant cypress test --- .../components/DrawerBody/DrawerBody.cy.tsx | 45 ------------------- 1 file changed, 45 deletions(-) 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 9de3ced6680db..eabf044228da6 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 @@ -84,49 +84,4 @@ describe('DrawerBody', () => { // wait for any rAF-based updates and then assert the scrollState .then(() => cy.get('#scroll-state').should('have.text', 'bottom')); }); - - it('treats near-bottom fractional scroll values as bottom', () => { - const Example = () => { - const context = useDrawerContextValue(); - - return ( - -
{context.scrollState}
- - Content - -
- ); - }; - - mountFluent(); - - cy.get('#drawer-body').then($e => { - const element = $e[0] as HTMLDivElement; - let mockedScrollTop = 89.4; - - Object.defineProperty(element, 'clientHeight', { - value: 100, - configurable: true, - }); - - Object.defineProperty(element, 'scrollHeight', { - value: 190, - configurable: true, - }); - - Object.defineProperty(element, 'scrollTop', { - configurable: true, - get: () => mockedScrollTop, - set: value => { - mockedScrollTop = value; - }, - }); - - mockedScrollTop = 89.4; - element.dispatchEvent(new Event('scroll', { bubbles: true })); - }); - - cy.get('#scroll-state').should('have.text', 'bottom'); - }); }); From 2ca232d062f89cd1ae75b9454002235769afc983 Mon Sep 17 00:00:00 2001 From: Aidam1 Date: Thu, 16 Jul 2026 11:43:50 +0200 Subject: [PATCH 5/5] refactor(react-drawer): clarify scroll-bottom tolerance per review feedback Co-authored-by: PaulGMardling --- .../library/src/components/DrawerBody/useDrawerBody.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) 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 a8a6e895cb62b..e2852d8e4caaa 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 @@ -16,8 +16,10 @@ import type { DrawerScrollState } from '../../shared/DrawerBase.types'; import type { DrawerBodyProps, DrawerBodyState } from './DrawerBody.types'; import { useFluent_unstable as useFluent } from '@fluentui/react-shared-contexts'; -// Allow a 1px tolerance so fractional scroll metrics (browser zoom, display scaling) -// still resolve to 'bottom' instead of getting stuck at 'middle' +/** + * Treat values within 1px of the scroll height as "bottom" to account for + * fractional scroll measurements caused by browser zoom and display scaling. + */ const SCROLL_BOTTOM_TOLERANCE = 1; /** @@ -35,7 +37,9 @@ export const getScrollState = ({ scrollTop, scrollHeight, clientHeight }: HTMLEl return 'top'; } - if (scrollTop + clientHeight >= scrollHeight - SCROLL_BOTTOM_TOLERANCE) { + const distanceFromBottom = scrollHeight - (scrollTop + clientHeight); + + if (distanceFromBottom <= SCROLL_BOTTOM_TOLERANCE) { return 'bottom'; }