diff --git a/web/client/components/misc/Dialog.jsx b/web/client/components/misc/Dialog.jsx index 1824da7e32f..f0d943e30b2 100644 --- a/web/client/components/misc/Dialog.jsx +++ b/web/client/components/misc/Dialog.jsx @@ -48,6 +48,11 @@ class Dialog extends React.Component { bounds: 'parent' }; + constructor(props) { + super(props); + this.mouseDownInside = false; + } + renderLoading = () => { if (this.props.maskLoading) { return (
) : body; let containerStyle = Object.assign({}, this.props.style.display ? {display: this.props.style.display} : {}, this.props.backgroundStyle); return this.props.modal ? -
{ this.mask = mask; }} onClick={this.onClickOut} style={containerStyle} className={"fade in modal " + this.props.containerClassName} role="dialog"> +
{ this.mask = mask; }} onMouseDown={this.onMouseDown} onClick={this.onClickOut} style={containerStyle} className={"fade in modal " + this.props.containerClassName} role="dialog"> {dialog}
: dialog; @@ -101,8 +106,13 @@ class Dialog extends React.Component { hasRole = (role) => { return React.Children.toArray(this.props.children).filter((child) => child.props.role === role).length > 0; }; + + onMouseDown = (e) => { + this.mouseDownInside = this.mask && this.mask.contains(e.target) && this.mask !== e.target; + }; + onClickOut = (e) => { - if (this.props.onClickOut && this.mask === e.target) { + if (this.props.onClickOut && this.mask === e.target && !this.mouseDownInside) { this.props.onClickOut(e); } }; diff --git a/web/client/components/misc/__tests__/Dialog-test.jsx b/web/client/components/misc/__tests__/Dialog-test.jsx new file mode 100644 index 00000000000..6b7fab6b0ce --- /dev/null +++ b/web/client/components/misc/__tests__/Dialog-test.jsx @@ -0,0 +1,130 @@ +/* + * Copyright 2026, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React from 'react'; +import ReactDOM from 'react-dom'; +import ReactTestUtils from 'react-dom/test-utils'; +import expect from 'expect'; + +import Dialog from '../Dialog'; + +describe('Dialog component', () => { + beforeEach((done) => { + document.body.innerHTML = '
'; + setTimeout(done); + }); + + afterEach((done) => { + ReactDOM.unmountComponentAtNode(document.getElementById("container")); + document.body.innerHTML = ''; + setTimeout(done); + }); + + it('renders with defaults', () => { + ReactDOM.render(, document.getElementById('container')); + const dialog = document.getElementById('dialog-test'); + expect(dialog).toExist(); + + const container = document.querySelector('.modal-dialog-container'); + expect(container).toExist(); + expect(container.className).toInclude('modal-dialog modal-content'); + expect(container.className).toInclude('modal-dialog-draggable'); + }); + + it('renders with modal', () => { + ReactDOM.render(, document.getElementById('container')); + const modal = document.querySelector('.modal'); + expect(modal).toExist(); + expect(modal.className).toInclude('fade in modal'); + + const dialog = document.getElementById('dialog-test'); + expect(dialog).toExist(); + }); + + it('renders roles (header, body, footer)', () => { + ReactDOM.render( + +
Header
+
Body
+
Footer
+
, + document.getElementById('container') + ); + + const header = document.querySelector('.modal-header .test-header'); + expect(header).toExist(); + + const body = document.querySelector('.modal-body .test-body'); + expect(body).toExist(); + + const footer = document.querySelector('.modal-footer .test-footer'); + expect(footer).toExist(); + }); + + it('renders without draggable', () => { + ReactDOM.render(, document.getElementById('container')); + const dialog = document.getElementById('dialog-test'); + expect(dialog).toExist(); + expect(dialog.className.indexOf('modal-dialog-draggable')).toBe(-1); + }); + + it('renders with maskLoading', () => { + ReactDOM.render( + +
Body
+
, + document.getElementById('container') + ); + + const spinner = document.querySelector('.spinner'); + expect(spinner).toExist(); + }); + + it('triggers onClickOut when clicking on the mask', () => { + const actions = { + onClickOut: () => {} + }; + const spy = expect.spyOn(actions, 'onClickOut'); + + ReactDOM.render( + , + document.getElementById('container') + ); + + const modal = document.querySelector('.modal'); + expect(modal).toExist(); + + ReactTestUtils.Simulate.click(modal); + + expect(spy).toHaveBeenCalled(); + }); + + it('does not trigger onClickOut when clicking inside the dialog', () => { + const actions = { + onClickOut: () => {} + }; + const spy = expect.spyOn(actions, 'onClickOut'); + + ReactDOM.render( + +
+
, + document.getElementById('container') + ); + + const modal = document.querySelector('.modal'); + expect(modal).toExist(); + + const innerButton = document.getElementById('inner-button'); + expect(innerButton).toExist(); + + ReactTestUtils.Simulate.click(innerButton); + + expect(spy).toNotHaveBeenCalled(); + }); +});