Skip to content

Commit 93772fb

Browse files
authored
feat(ui5-dialog): introduce draggable property (#2269)
Part of #2082
1 parent 2faad25 commit 93772fb

File tree

6 files changed

+199
-16
lines changed

6 files changed

+199
-16
lines changed

packages/base/src/util/isValidPropertyName.js

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const allowList = [
44
"disabled",
55
"title",
66
"hidden",
7+
"draggable",
78
];
89

910
/**

packages/main/src/Dialog.hbs

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
{{>include "./Popup.hbs"}}
22

33
{{#*inline "beforeContent"}}
4-
<header class="ui5-popup-header-root" id="ui5-popup-header">
4+
<header
5+
class="ui5-popup-header-root"
6+
id="ui5-popup-header"
7+
@mousedown="{{_onDragMouseDown}}">
58
{{#if header.length }}
69
<slot name="header"></slot>
710
{{else}}

packages/main/src/Dialog.js

+126-6
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { isPhone } from "@ui5/webcomponents-base/dist/Device.js";
1+
import { isPhone, isDesktop } from "@ui5/webcomponents-base/dist/Device.js";
22
import Popup from "./Popup.js";
33

44
// Template
@@ -52,7 +52,7 @@ const metadata = {
5252
/**
5353
* Determines whether the <code>ui5-dialog</code> should be stretched to fullscreen.
5454
* <br><br>
55-
* <b>Note:</b> The <code>ui5-dialog</code> will be stretched to aproximetly
55+
* <b>Note:</b> The <code>ui5-dialog</code> will be stretched to approximately
5656
* 90% of the viewport.
5757
*
5858
* @type {boolean}
@@ -63,12 +63,32 @@ const metadata = {
6363
type: Boolean,
6464
},
6565

66+
/**
67+
* Determines whether the <code>ui5-dialog</code> is draggable.
68+
* If this property is set to true, the Dialog will be draggable by its header.
69+
* <br><br>
70+
* <b>Note:</b> The <code>ui5-dialog</code> can be draggable only in desktop mode.
71+
* @defaultvalue false
72+
* @since 1.0.0-rc.9
73+
* @public
74+
*/
75+
draggable: {
76+
type: Boolean,
77+
},
78+
6679
/**
6780
* @private
6881
*/
6982
onPhone: {
7083
type: Boolean,
7184
},
85+
86+
/**
87+
* @private
88+
*/
89+
onDesktop: {
90+
type: Boolean,
91+
},
7292
},
7393
};
7494

@@ -89,6 +109,9 @@ const metadata = {
89109
* <h3>Structure</h3>
90110
* A <code>ui5-dialog</code> consists of a header, content, and a footer for action buttons.
91111
* The <code>ui5-dialog</code> is usually displayed at the center of the screen.
112+
* Its position can be changed by the user. To enable this, you need to set the property <code>draggable</code> accordingly.
113+
114+
92115
*
93116
* <h3>Responsive Behavior</h3>
94117
* The <code>stretch</code> property can be used to stretch the
@@ -121,10 +144,6 @@ class Dialog extends Popup {
121144
return [PopupsCommonCss, dialogCSS];
122145
}
123146

124-
onBeforeRendering() {
125-
this.onPhone = isPhone();
126-
}
127-
128147
get isModal() { // Required by Popup.js
129148
return true;
130149
}
@@ -147,6 +166,107 @@ class Dialog extends Popup {
147166
},
148167
};
149168
}
169+
170+
onBeforeRendering() {
171+
this.onPhone = isPhone();
172+
this.onDesktop = isDesktop();
173+
}
174+
175+
onEnterDOM() {
176+
this._dragMouseMoveHandler = this._onDragMouseMove.bind(this);
177+
this._dragMouseUpHandler = this._onDragMouseUp.bind(this);
178+
}
179+
180+
onExitDOM() {
181+
this._dragMouseMoveHandler = null;
182+
this._dragMouseUpHandler = null;
183+
}
184+
185+
/**
186+
* Event handlers
187+
*/
188+
_onDragMouseDown(event) {
189+
if (!(this.draggable && this.onDesktop)) {
190+
return;
191+
}
192+
193+
// only allow dragging on the header's whitespace
194+
if (!event.target.classList.contains("ui5-popup-header-root")
195+
&& event.target.getAttribute("slot") !== "header") {
196+
return;
197+
}
198+
199+
event.preventDefault();
200+
201+
const {
202+
top,
203+
left,
204+
} = this.getBoundingClientRect();
205+
const {
206+
width,
207+
height,
208+
} = window.getComputedStyle(this);
209+
210+
Object.assign(this.style, {
211+
transform: "none",
212+
top: `${top}px`,
213+
left: `${left}px`,
214+
width: `${Math.round(Number(width) * 100) / 100}px`,
215+
height: `${Math.round(Number(height) * 100) / 100}px`,
216+
});
217+
218+
this._x = event.clientX;
219+
this._y = event.clientY;
220+
221+
this._attachDragHandlers();
222+
}
223+
224+
_onDragMouseMove(event) {
225+
event.preventDefault();
226+
227+
const calcX = this._x - event.clientX;
228+
const calcY = this._y - event.clientY;
229+
const {
230+
left,
231+
top,
232+
} = this.getBoundingClientRect();
233+
234+
235+
Object.assign(this.style, {
236+
left: `${Math.floor(left - calcX)}px`,
237+
top: `${Math.floor(top - calcY)}px`,
238+
});
239+
240+
this._x = event.clientX;
241+
this._y = event.clientY;
242+
}
243+
244+
_onDragMouseUp() {
245+
this._x = null;
246+
this._y = null;
247+
248+
this._detachDragHandlers();
249+
}
250+
251+
_attachDragHandlers() {
252+
window.addEventListener("mousemove", this._dragMouseMoveHandler);
253+
window.addEventListener("mouseup", this._dragMouseUpHandler);
254+
this.addEventListener("ui5-before-close", this._recenter);
255+
}
256+
257+
_detachDragHandlers() {
258+
window.removeEventListener("mousemove", this._dragMouseMoveHandler);
259+
window.removeEventListener("mouseup", this._dragMouseUpHandler);
260+
}
261+
262+
_recenter() {
263+
Object.assign(this.style, {
264+
top: "",
265+
left: "",
266+
transform: "",
267+
});
268+
this.removeEventListener("ui5-before-close", this._recenter);
269+
}
150270
}
151271

152272
Dialog.define();

packages/main/src/themes/Dialog.css

+9
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,15 @@
2121
max-width: 100vw;
2222
}
2323

24+
:host([draggable]) .ui5-popup-header-root,
25+
:host([draggable]) ::slotted([slot="header"]) {
26+
cursor: move;
27+
}
28+
29+
:host([draggable]) .ui5-popup-header-root * {
30+
cursor: auto;
31+
}
32+
2433
.ui5-popup-root {
2534
display: flex;
2635
flex-direction: column;

packages/main/test/pages/Dialog.html

+24-7
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,15 @@
3838
<br>
3939
<br>
4040
<ui5-button id="empty-open">Open empty dialog (no focusable element)</ui5-button>
41-
<br><br>
41+
<br>
42+
<br>
4243
<ui5-button id="wide-open">Open wide dialog</ui5-button>
43-
<br><br>
44+
<br>
45+
<br>
4446
<ui5-button id="wide-open2">Open wide dialog 2</ui5-button>
45-
<br><br>
47+
<br>
48+
<br>
49+
<ui5-button id="draggable-open">Open draggable dialog</ui5-button>
4650

4751
<ui5-block-layer></ui5-block-layer>
4852

@@ -185,6 +189,17 @@
185189
</div>
186190
</ui5-dialog>
187191

192+
<ui5-dialog id="draggable-dialog" draggable>
193+
<div slot="header" style="flex: 1 0 0; height: 2.5rem;">Draggable dialog</div>
194+
195+
<p>Move this dialog around the screen by dragging it by its header.</p>
196+
<p>This feature available only on Desktop.</p>
197+
198+
<div slot="footer" style="display: flex; justify-content: flex-end; width: 100%; padding: .25rem 1rem;">
199+
<ui5-button id="draggable-close" design="Emphasized">OK</ui5-button>
200+
</div>
201+
</ui5-dialog>
202+
188203
<ui5-popover header-text="My Heading" id="pop" style="width: 300px" placement-type="Top">
189204
<!-- <div slot="header">
190205
Hello World
@@ -247,7 +262,6 @@
247262
<ui5-dialog id="empty-dialog">Empty</ui5-dialog>
248263

249264
<script>
250-
251265
let preventClosing = true;
252266

253267
btnOpenDialog.addEventListener("click", function () {
@@ -297,9 +311,12 @@
297311
bigDangerPop.openBy(bigDanger);
298312
});
299313

300-
window["empty-open"].addEventListener("click", function (event) { window["empty-dialog"].open(); });
301-
window["wide-open"].addEventListener("click", function (event) { window["wide-dialog"].open(); });
302-
window["wide-open2"].addEventListener("click", function (event) { window["wide-dialog2"].open(); });
314+
window["empty-open"].addEventListener("click", function () { window["empty-dialog"].open(); });
315+
window["wide-open"].addEventListener("click", function () { window["wide-dialog"].open(); });
316+
window["wide-open2"].addEventListener("click", function () { window["wide-dialog2"].open(); });
317+
window["draggable-open"].addEventListener("click", function () { window["draggable-dialog"].open(); });
318+
window["draggable-close"].addEventListener("click", function () { window["draggable-dialog"].close(); });
319+
303320
</script>
304321
</body>
305322

packages/main/test/specs/Dialog.spec.js

+35-2
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ describe("Dialog general interaction", () => {
2626
btnOpenDialog.click();
2727
select.click();
2828

29-
const dialogZIndex = parseInt(browser.$("#dialog").getCSSProperty('z-index').value);
30-
const popoverZIndex = parseInt(browser.$(`.${select.getProperty("_id")}`).shadow$("ui5-responsive-popover").getCSSProperty('z-index').value);
29+
const dialogZIndex = parseInt(browser.$("#dialog").getCSSProperty("z-index").value);
30+
const popoverZIndex = parseInt(browser.$(`.${select.getProperty("_id")}`).shadow$("ui5-responsive-popover").getCSSProperty("z-index").value);
3131

3232
assert.ok(popoverZIndex > dialogZIndex, "Popover is above dialog.");
3333
});
@@ -47,6 +47,39 @@ describe("Dialog general interaction", () => {
4747

4848
assert.ok(!browser.$("ui5-static-area").length, "No static area.");
4949
});
50+
51+
it("draggable", () => {
52+
browser.url("http://localhost:8080/test-resources/pages/Dialog.html");
53+
54+
const openDraggableDialogButton = browser.$("#draggable-open");
55+
openDraggableDialogButton.click();
56+
57+
const dialog = browser.$("#draggable-dialog");
58+
const topBeforeDragging = parseInt(dialog.getCSSProperty("top").value);
59+
const leftBeforeDragging = parseInt(dialog.getCSSProperty("left").value);
60+
61+
const header = browser.$("#draggable-dialog").shadow$(".ui5-popup-header-root");
62+
63+
header.dragAndDrop({ x: -200, y: -200 });
64+
65+
const topAfterDragging = parseInt(dialog.getCSSProperty("top").value);
66+
const leftAfterDragging = parseInt(dialog.getCSSProperty("left").value);
67+
68+
assert.notStrictEqual(topBeforeDragging, topAfterDragging, "top position has changed");
69+
assert.notStrictEqual(leftBeforeDragging, leftAfterDragging, "left position has changed");
70+
71+
browser.keys("Enter");
72+
73+
openDraggableDialogButton.click();
74+
75+
const topAfterReopening = parseInt(dialog.getCSSProperty("top").value);
76+
const leftAfterReopening = parseInt(dialog.getCSSProperty("left").value);
77+
78+
assert.strictEqual(topBeforeDragging, topAfterReopening, "top position has been reset back to initial");
79+
assert.strictEqual(leftBeforeDragging, leftAfterReopening, "left position has been reset back to initial");
80+
81+
browser.keys("esc");
82+
});
5083
});
5184

5285

0 commit comments

Comments
 (0)