diff --git a/app/src/Assets/javascript/window-chrome.js b/app/src/Assets/javascript/window-chrome.js index b8340c2..3f1b824 100644 --- a/app/src/Assets/javascript/window-chrome.js +++ b/app/src/Assets/javascript/window-chrome.js @@ -22,6 +22,49 @@ const CONTROLS = "a, button, input, select, textarea, label, summary, [role=button], [tabindex], [contenteditable], dialog"; + // A press on a modal's backdrop lands on the dialog itself, whose content is all inside its box. + const onBackdrop = (event) => { + const target = /** @type {Element} */ (event.target); + if (!target.matches("dialog")) { + return false; + } + const box = target.getBoundingClientRect(); + return ( + event.clientX < box.left || + event.clientX > box.right || + event.clientY < box.top || + event.clientY > box.bottom + ); + }; + + // The backdrop still closes the dialog on a click, and a window drag takes the release, so the + // drag waits for the pointer to really move. preventDefault() leaves the click alone. + const dragOnceMoved = (press) => { + press.preventDefault(); + const stop = () => { + removeEventListener("mousemove", move); + removeEventListener("mouseup", stop); + }; + const move = (event) => { + // A lost release would leave this listening, and the next drag anywhere would move the window. + if (!event.buttons) { + stop(); + return; + } + if ( + Math.hypot( + event.clientX - press.clientX, + event.clientY - press.clientY, + ) >= 3 + ) { + stop(); + window.adminer.native?.dragWindow(); + } + }; + addEventListener("mousemove", move); + addEventListener("mouseup", stop); + }; + // The toolbar's hairline shows only once the page scrolls under it, or while the window is not // the active one, as Mail's does (window.css): two classes on to draw it by. if (window.adminer.native?.dragWindow) { @@ -43,11 +86,17 @@ if (window.adminer.native?.dragWindow) { document.addEventListener("mousedown", (event) => { const target = /** @type {Element} */ (event.target); + // Full screen has no title bar to stand in for. + if (event.button !== 0 || root.classList.contains("ad-fullscreen")) { + return; + } + if (document.querySelector("dialog[open]")) { + if (event.clientY < band() && onBackdrop(event)) { + dragOnceMoved(event); + } + return; + } if ( - event.button !== 0 || - // Full screen has no title bar to stand in for, and a dialog's backdrop closes it. - root.classList.contains("ad-fullscreen") || - document.querySelector("dialog[open]") || !(target === document.body || event.clientY < band()) || target.closest(CONTROLS) ) { diff --git a/tests/e2e/bootstrap/DesktopContext.php b/tests/e2e/bootstrap/DesktopContext.php index fcce0a1..d0e742f 100644 --- a/tests/e2e/bootstrap/DesktopContext.php +++ b/tests/e2e/bootstrap/DesktopContext.php @@ -3221,6 +3221,15 @@ public function clickToolbarSearch(): void )->getByRole('button', ['name' => 'Search'])->click(); } + #[Then('the search box is closed')] + public function searchBoxIsClosed(): void + { + $this->page->waitForFunction( + // language=JavaScript + "() => !document.querySelector('#desktop-search').open", + ); + } + #[Then('the search box is open')] public function searchBoxIsOpen(): void { @@ -3444,6 +3453,34 @@ public function pressEmptyToolbar(): void $this->page->mouse()->click((float) $box['x'], (float) $box['y']); } + #[When('I drag the mouse along the backdrop in the toolbar\'s band')] + public function dragBackdropInBand(): void + { + $this->page->mouse()->move(100, 10); + $this->page->mouse()->down(); + $this->page->mouse()->move(140, 10); + $this->page->mouse()->up(); + } + + /** A real press, then the move a browser would report once the button is up: no release event. */ + #[When('I press the backdrop in the toolbar\'s band and the release gets lost')] + public function pressBackdropReleaseLost(): void + { + $this->page->mouse()->move(100, 10); + $this->page->mouse()->down(); + $this->page->evaluate( + // language=JavaScript + "() => dispatchEvent(new MouseEvent('mousemove', { clientX: 140, clientY: 10, buttons: 0 }))", + ); + $this->page->mouse()->up(); + } + + #[When('I click the backdrop in the toolbar\'s band')] + public function clickBackdropInBand(): void + { + $this->page->mouse()->click(100, 10); + } + #[When('I open the toolbar\'s sort menu')] public function openSortMenu(): void { diff --git a/tests/e2e/features/shell/window.feature b/tests/e2e/features/shell/window.feature index 24e1adc..cbfb707 100644 --- a/tests/e2e/features/shell/window.feature +++ b/tests/e2e/features/shell/window.feature @@ -50,6 +50,30 @@ Feature: The window without a title bar When I press the mouse on one of the toolbar's buttons Then the window was asked to move 1 time + Scenario: With a modal open the band above it still moves the window, and a click there does not + When I open the "users" table + And I open the settings dialog + And I drag the mouse along the backdrop in the toolbar's band + Then the window was asked to move 1 time + And the settings dialog is open + When I click the backdrop in the toolbar's band + Then the window was asked to move 1 time + And the settings dialog is open + + Scenario: A click on the band above a dialog that closes on a click outside closes it, without moving the window + When I open the "users" table + And I click the toolbar's search + Then the search box is open + When I click the backdrop in the toolbar's band + Then the search box is closed + And the window was asked to move 0 times + + Scenario: A move with no button down does not move the window, if the release above a modal got lost + When I open the "users" table + And I open the settings dialog + And I press the backdrop in the toolbar's band and the release gets lost + Then the window was asked to move 0 times + Scenario: The sidebar stays hidden across pages, its toggle beside the lights When I open the "users" table And I hide the sidebar