Skip to content

Commit 38c192d

Browse files
[Bugfix] Bugfix/mutation observer loop 11.7 (#11623)
(r11.7 → 11.7)
1 parent 0ac2ede commit 38c192d

File tree

3 files changed

+75
-42
lines changed

3 files changed

+75
-42
lines changed

src/components/ModularComponents/AppStories/App.stories.js

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@ import App from 'components/App';
22
import { mockHeadersNormalized, mockModularComponents, mockLeftHeader } from './mockAppState';
33
import { userEvent, within, expect, waitFor } from 'storybook/test';
44
import { defaultModularComponents, defaultModularHeaders } from 'src/redux/modularComponents';
5-
import { createTemplate } from 'helpers/storybookHelper';
5+
import { createTemplate, MockApp } from 'helpers/storybookHelper';
66
import initialState from 'src/redux/initialState';
77
import actions from 'actions';
88
import { VIEWER_CONFIGURATIONS } from 'constants/customizationVariables';
9-
9+
import React from 'react';
1010
export default {
1111
title: 'ModularComponents/App',
1212
component: App,
@@ -451,4 +451,28 @@ AppStashSwitchStory.play = async ({ canvasElement }) => {
451451
storeRef.current.dispatch(actions.restoreComponents(VIEWER_CONFIGURATIONS.DEFAULT));
452452
const newItem = await canvas.findByRole('button', { name: newName });
453453
await expect(newItem).toBeInTheDocument();
454+
};
455+
456+
export const AppFlyoutResponsiveTest = (args, context) => {
457+
return (
458+
<MockApp initialState={initialState} width={500} height={800} />
459+
);
460+
};
461+
462+
AppFlyoutResponsiveTest.play = async ({ canvasElement }) => {
463+
const canvas = within(canvasElement);
464+
const zoomToggleButton = await canvas.findByRole('button', { name: 'Zoom Options' });
465+
await userEvent.click(zoomToggleButton);
466+
const flyout = await canvas.findByRole('button', { name: /Zoom: 100%/ });
467+
await expect(flyout).toBeVisible();
468+
};
469+
470+
AppFlyoutResponsiveTest.parameters = {
471+
layout: 'fullscreen',
472+
chromatic: {
473+
delay: 2000,
474+
modes: {
475+
'Light theme RTL': { disable: true },
476+
},
477+
}
454478
};

src/components/ModularComponents/Flyout/Flyout.js

Lines changed: 47 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import Icon from 'components/Icon';
1919
import './Flyout.scss';
2020
import { Swipeable } from 'react-swipeable';
2121
import getAppRect from 'helpers/getAppRect';
22-
import debounce from 'lodash/debounce';
2322

2423
const Flyout = () => {
2524
const { t } = useTranslation();
@@ -68,64 +67,73 @@ const Flyout = () => {
6867

6968
useLayoutEffect(() => {
7069
const tempRefElement = getElementDOMRef(toggleElement);
71-
const appRect = getAppRect();
72-
const maxHeightValue = appRect.height - horizontalHeadersUsedHeight;
73-
setMaxHeightValue(maxHeightValue);
7470

75-
// Check if the element is in the dom or invisible
71+
// Check if the element is in the DOM or invisible
7672
if (tempRefElement && tempRefElement.offsetParent === null) {
7773
return;
7874
}
7975

80-
const calculateAndSetPosition = () => {
81-
const tempRefElement = getElementDOMRef(toggleElement);
82-
const appRect = getAppRect();
83-
const correctedPosition = { x: position.x, y: position.y };
84-
// Check if toggleElement is not null
85-
if (toggleElement && tempRefElement) {
76+
const calculateAndMaybeSetPosition = () => {
77+
const refEl = getElementDOMRef(toggleElement);
78+
const app = getAppRect();
79+
// Keep max height in sync with the exact app rect used for positioning
80+
setMaxHeightValue(app.height - horizontalHeadersUsedHeight);
81+
const next = { x: position.x, y: position.y };
82+
83+
if (toggleElement && refEl) {
8684
const { x, y } = getFlyoutPositionOnElement(toggleElement, flyoutRef);
87-
correctedPosition.x = x;
88-
correctedPosition.y = y;
85+
next.x = x;
86+
next.y = y;
8987
}
88+
9089
const flyoutRect = flyoutRef.current?.getBoundingClientRect();
91-
if (flyoutRect) {
90+
if (flyoutRect && app) {
9291
const PADDING = 5;
93-
const widthOverflow = correctedPosition.x + flyoutRect.width + PADDING - appRect.right;
94-
const heightOverflow = correctedPosition.y + flyoutRect.height + PADDING - appRect.bottom;
92+
const widthOverflow = next.x + flyoutRect.width + PADDING - app.right;
93+
const heightOverflow = next.y + flyoutRect.height + PADDING - app.bottom;
9594
if (widthOverflow > 0) {
96-
correctedPosition.x -= widthOverflow;
95+
next.x -= widthOverflow;
9796
}
9897
if (heightOverflow > 0) {
99-
correctedPosition.y -= heightOverflow;
98+
next.y -= heightOverflow;
10099
}
101-
if (correctedPosition.x < PADDING) {
102-
correctedPosition.x = PADDING;
100+
if (next.x < PADDING) {
101+
next.x = PADDING;
103102
}
104-
if (correctedPosition.y < PADDING) {
105-
correctedPosition.y = PADDING;
103+
if (next.y < PADDING) {
104+
next.y = PADDING;
106105
}
107106
}
108-
setCorrectedPosition(correctedPosition);
107+
108+
setCorrectedPosition((prev) => {
109+
if (!prev || prev.x !== next.x || prev.y !== next.y) {
110+
return next;
111+
}
112+
return prev;
113+
});
109114
};
110115

111-
// Wait for flyout to render all items before calculating position
116+
// Run once now and once on the next frame to catch late layout
112117
if (flyoutRef.current) {
113-
let observer;
114-
const disconnect = debounce(
115-
() => observer.disconnect(),
116-
100, { leading: false, trailing: true });
117-
const setPosition = () => {
118-
calculateAndSetPosition();
119-
disconnect();
120-
};
121-
// Set position at multiple stages to minimize flickering
122-
observer = new MutationObserver(setPosition);
123-
observer.observe(flyoutRef.current, { attributes: true, childList: true, subtree: true });
124-
setPosition();
125-
requestAnimationFrame(setPosition);
126-
return () => observer.disconnect();
118+
calculateAndMaybeSetPosition();
119+
requestAnimationFrame(calculateAndMaybeSetPosition);
127120
}
128-
}, [activeItem, position, items, inputValue]);
121+
122+
let resizeObserver;
123+
124+
if (typeof ResizeObserver !== 'undefined' && flyoutRef.current) {
125+
resizeObserver = new ResizeObserver(() => {
126+
calculateAndMaybeSetPosition();
127+
});
128+
resizeObserver.observe(flyoutRef.current);
129+
}
130+
131+
return () => {
132+
if (resizeObserver) {
133+
resizeObserver.disconnect();
134+
}
135+
};
136+
}, [activePath, position, items, inputValue, isFlyoutOpen]);
129137

130138
useLayoutEffect(() => {
131139
const appRect = getAppRect();

src/components/ModularComponents/FlyoutItemContainer/FlyoutItemContainer.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ const FlyoutItemContainer = forwardRef((props, ref) => {
6363

6464
const flyoutItemLabel = label ?? title;
6565
const finalLabel = typeof flyoutItemLabel === 'string' ? t(flyoutItemLabel) : flyoutItemLabel;
66+
const finalLabelString = typeof finalLabel === 'string' ? finalLabel : null;
6667
const isSelected = props.additionalClass === 'active';
6768
return (
6869
<button
@@ -72,7 +73,7 @@ const FlyoutItemContainer = forwardRef((props, ref) => {
7273
aria-disabled={disabled}
7374
onKeyDown={onKeyDownHandler}
7475
data-element={dataElement}
75-
aria-label={finalLabel}
76+
aria-label={finalLabelString}
7677
aria-pressed={isSelected}
7778
>
7879
<div className="icon-label-wrapper">

0 commit comments

Comments
 (0)