diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5e9b3c2110..951c37a180 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,12 @@
The following is a curated list of changes in the Enact sandstone module, newest changes on the top.
+## [unreleased]
+
+### Fixed
+
+- `sandstone/ContextualPopupDecorator` to update popup position properly when the screen orientation change
+
## [2.7.18] - 2024-09-05
### Added
diff --git a/ContextualPopupDecorator/ContextualPopupDecorator.js b/ContextualPopupDecorator/ContextualPopupDecorator.js
index 85d2fc068d..104df5e991 100644
--- a/ContextualPopupDecorator/ContextualPopupDecorator.js
+++ b/ContextualPopupDecorator/ContextualPopupDecorator.js
@@ -1,3 +1,5 @@
+/* global ResizeObserver */
+
/**
* A higher-order component to add a Sandstone styled popup to a component.
*
@@ -271,6 +273,7 @@ const Decorator = hoc(defaultConfig, (config, Wrapped) => {
activator: null
};
+ this.resizeObserver = null;
this.overflow = {};
this.adjustedDirection = this.props.direction;
this.id = this.generateId();
@@ -290,6 +293,12 @@ const Decorator = hoc(defaultConfig, (config, Wrapped) => {
on('keydown', this.handleKeyDown);
on('keyup', this.handleKeyUp);
}
+
+ if (typeof ResizeObserver === 'function') {
+ this.resizeObserver = new ResizeObserver(() => {
+ this.positionContextualPopup();
+ });
+ }
}
getSnapshotBeforeUpdate (prevProps, prevState) {
@@ -339,6 +348,11 @@ const Decorator = hoc(defaultConfig, (config, Wrapped) => {
off('keyup', this.handleKeyUp);
}
Spotlight.remove(this.state.containerId);
+
+ if (this.resizeObserver) {
+ this.resizeObserver.disconnect();
+ this.resizeObserver = null;
+ }
}
generateId = () => {
@@ -594,6 +608,18 @@ const Decorator = hoc(defaultConfig, (config, Wrapped) => {
getClientNode = (node) => {
this.clientNode = ReactDOM.findDOMNode(node); // eslint-disable-line react/no-find-dom-node
+
+ if (this.resizeObserver) {
+ if (node) {
+ // It is not easy to trigger changed position of activator,
+ // so we chose to observe the `div` element's size that has the real size below the root of floatLayer.
+ // This implementation is dependent on the current structure of FloatingLayer,
+ // so if the structure have changed, below code needs to be changed accordingly.
+ this.resizeObserver.observe(node?.parentElement?.parentElement);
+ } else {
+ this.resizeObserver.disconnect();
+ }
+ }
};
handle = handle.bind(this);
diff --git a/ContextualPopupDecorator/tests/ContextualPopupDecorator-specs.js b/ContextualPopupDecorator/tests/ContextualPopupDecorator-specs.js
index 03a6562776..2c4e625e40 100644
--- a/ContextualPopupDecorator/tests/ContextualPopupDecorator-specs.js
+++ b/ContextualPopupDecorator/tests/ContextualPopupDecorator-specs.js
@@ -505,4 +505,40 @@ describe('ContextualPopupDecorator Specs', () => {
expect(scrimDivFirst).toHaveClass(expectedFirst);
expect(scrimDivSecond).toHaveClass(expectedSecond);
});
+
+ test('should create and observe with `ResizeObserver` when the popup opened and disconnect when the popup closed', () => {
+ const originalObserver = global.ResizeObserver;
+
+ const MockObserverInstance = {
+ observe: jest.fn(),
+ disconnect: jest.fn()
+ };
+ global.ResizeObserver = jest.fn().mockImplementation(() => MockObserverInstance);
+
+ const Root = FloatingLayerDecorator('div');
+ const {rerender} = render(
+
+ }>
+ Hello
+
+
+ );
+
+ const contextualButton = screen.getByTestId('contextualButton');
+
+ expect(contextualButton).toBeInTheDocument();
+ expect(MockObserverInstance.observe).toHaveBeenCalled();
+
+ rerender(
+
+ }>
+ Hello
+
+
+ );
+
+ expect(MockObserverInstance.disconnect).toHaveBeenCalled();
+
+ global.ResizeObserver = originalObserver;
+ });
});