Skip to content

Commit 8580fd8

Browse files
authored
feat(ui5-dialog): add keyboard support for draggable and resizable (#3483)
1 parent 1550c6d commit 8580fd8

File tree

14 files changed

+331
-43
lines changed

14 files changed

+331
-43
lines changed

packages/base/hash.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
Jh82i3fpuuAQEHSWZLSJ9IF817M=
1+
dsWZPIURDa/FQdScEGl9wVxGL1Y=

packages/base/src/Keys.js

+6
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,10 @@ const isUpShift = event => (event.key ? (event.key === "ArrowUp" || event.key ==
131131

132132
const isDownShift = event => (event.key ? (event.key === "ArrowDown" || event.key === "Down") : event.keyCode === KeyCodes.ARROW_DOWN) && checkModifierKeys(event, false, false, true);
133133

134+
const isLeftShift = event => (event.key ? (event.key === "ArrowLeft" || event.key === "Left") : event.keyCode === KeyCodes.ARROW_LEFT) && checkModifierKeys(event, false, false, true);
135+
136+
const isRightShift = event => (event.key ? (event.key === "ArrowRight" || event.key === "Right") : event.keyCode === KeyCodes.ARROW_RIGHT) && checkModifierKeys(event, false, false, true);
137+
134138
const isUpShiftCtrl = event => (event.key ? (event.key === "ArrowUp" || event.key === "Up") : event.keyCode === KeyCodes.ARROW_UP) && checkModifierKeys(event, true, false, true);
135139

136140
const isDownShiftCtrl = event => (event.key ? (event.key === "ArrowDown" || event.key === "Down") : event.keyCode === KeyCodes.ARROW_DOWN) && checkModifierKeys(event, true, false, true);
@@ -208,6 +212,8 @@ export {
208212
isDownCtrl,
209213
isUpShift,
210214
isDownShift,
215+
isLeftShift,
216+
isRightShift,
211217
isUpShiftCtrl,
212218
isDownShiftCtrl,
213219
isHome,

packages/main/src/Dialog.hbs

+2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
<header
66
class="ui5-popup-header-root"
77
id="ui5-popup-header"
8+
tabindex="{{_headerTabIndex}}"
9+
@keydown="{{_onDragOrResizeKeyDown}}"
810
@mousedown="{{_onDragMouseDown}}">
911
{{#if header.length }}
1012
<slot name="header"></slot>

packages/main/src/Dialog.js

+139-28
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import { isPhone, isDesktop } from "@ui5/webcomponents-base/dist/Device.js";
22
import clamp from "@ui5/webcomponents-base/dist/util/clamp.js";
33
import ResizeHandler from "@ui5/webcomponents-base/dist/delegate/ResizeHandler.js";
4+
import {
5+
isUp, isDown, isLeft, isRight,
6+
isUpShift, isDownShift, isLeftShift, isRightShift,
7+
} from "@ui5/webcomponents-base/dist/Keys.js";
48
import Popup from "./Popup.js";
59
import "@ui5/webcomponents-icons/dist/resize-corner.js";
610
import Icon from "./Icon.js";
@@ -12,6 +16,11 @@ import browserScrollbarCSS from "./generated/themes/BrowserScrollbar.css.js";
1216
import PopupsCommonCss from "./generated/themes/PopupsCommon.css.js";
1317
import dialogCSS from "./generated/themes/Dialog.css.js";
1418

19+
/**
20+
* Defines the step size at which this component would change by when being dragged or resized with the keyboard.
21+
*/
22+
const STEP_SIZE = 16;
23+
1524
/**
1625
* @public
1726
*/
@@ -204,6 +213,10 @@ class Dialog extends Popup {
204213
return [browserScrollbarCSS, PopupsCommonCss, dialogCSS];
205214
}
206215

216+
static _isHeader(element) {
217+
return element.classList.contains("ui5-popup-header-root") || element.getAttribute("slot") === "header";
218+
}
219+
207220
/**
208221
* Opens the dialog
209222
*
@@ -251,13 +264,25 @@ class Dialog extends Popup {
251264
return "flex";
252265
}
253266

267+
/**
268+
* Determines if the header of the dialog should be shown.
269+
*/
254270
get _displayHeader() {
255-
return this.header.length || this.headerText;
271+
return this.header.length || this.headerText || this.draggable || this.resizable;
272+
}
273+
274+
get _movable() {
275+
return !this.stretch && this.onDesktop && (this.draggable || this.resizable);
276+
}
277+
278+
get _headerTabIndex() {
279+
return this._movable ? "0" : undefined;
256280
}
257281

258282
show() {
259283
super.show();
260284
this._center();
285+
this._attachResizeHandlers();
261286
}
262287

263288
onBeforeRendering() {
@@ -266,16 +291,17 @@ class Dialog extends Popup {
266291
this.onDesktop = isDesktop();
267292
}
268293

269-
onEnterDOM() {
270-
super.onEnterDOM();
294+
onExitDOM() {
295+
super.onExitDOM();
296+
this._detachResizeHandlers();
297+
}
271298

299+
_attachResizeHandlers() {
272300
ResizeHandler.register(this, this._screenResizeHandler);
273301
ResizeHandler.register(document.body, this._screenResizeHandler);
274302
}
275303

276-
onExitDOM() {
277-
super.onExitDOM();
278-
304+
_detachResizeHandlers() {
279305
ResizeHandler.deregister(this, this._screenResizeHandler);
280306
ResizeHandler.deregister(document.body, this._screenResizeHandler);
281307
}
@@ -304,13 +330,8 @@ class Dialog extends Popup {
304330
* Event handlers
305331
*/
306332
_onDragMouseDown(event) {
307-
if (!(this.draggable && this.onDesktop)) {
308-
return;
309-
}
310-
311-
// only allow dragging on the header's whitespace
312-
if (!event.target.classList.contains("ui5-popup-header-root")
313-
&& event.target.getAttribute("slot") !== "header") {
333+
// allow dragging only on the header
334+
if (!this._movable || !this.draggable || !Dialog._isHeader(event.target)) {
314335
return;
315336
}
316337

@@ -335,7 +356,7 @@ class Dialog extends Popup {
335356
this._x = event.clientX;
336357
this._y = event.clientY;
337358

338-
this._attachDragHandlers();
359+
this._attachMouseDragHandlers();
339360
}
340361

341362
_onDragMouseMove(event) {
@@ -361,24 +382,115 @@ class Dialog extends Popup {
361382
this._x = null;
362383
this._y = null;
363384

364-
this._detachDragHandlers();
385+
this._detachMouseDragHandlers();
365386
}
366387

367-
_attachDragHandlers() {
368-
ResizeHandler.deregister(this, this._screenResizeHandler);
369-
ResizeHandler.deregister(document.body, this._screenResizeHandler);
388+
_onDragOrResizeKeyDown(event) {
389+
if (!this._movable || !Dialog._isHeader(event.target)) {
390+
return;
391+
}
392+
393+
if (this.draggable && [isUp, isDown, isLeft, isRight].some(key => key(event))) {
394+
this._dragWithEvent(event);
395+
return;
396+
}
397+
398+
if (this.resizable && [isUpShift, isDownShift, isLeftShift, isRightShift].some(key => key(event))) {
399+
this._resizeWithEvent(event);
400+
}
401+
}
402+
403+
_dragWithEvent(event) {
404+
const {
405+
top,
406+
left,
407+
width,
408+
height,
409+
} = this.getBoundingClientRect();
410+
411+
let newPos,
412+
posDirection;
413+
414+
switch (true) {
415+
case isUp(event):
416+
newPos = top - STEP_SIZE;
417+
posDirection = "top";
418+
break;
419+
case isDown(event):
420+
newPos = top + STEP_SIZE;
421+
posDirection = "top";
422+
break;
423+
case isLeft(event):
424+
newPos = left - STEP_SIZE;
425+
posDirection = "left";
426+
break;
427+
case isRight(event):
428+
newPos = left + STEP_SIZE;
429+
posDirection = "left";
430+
break;
431+
}
432+
433+
newPos = clamp(
434+
newPos,
435+
0,
436+
posDirection === "left" ? window.innerWidth - width : window.innerHeight - height,
437+
);
438+
439+
this.style[posDirection] = `${newPos}px`;
440+
}
441+
442+
_resizeWithEvent(event) {
443+
this._detachResizeHandlers();
444+
this.addEventListener("ui5-before-close", this._revertSize);
445+
446+
const { top, left } = this.getBoundingClientRect(),
447+
style = window.getComputedStyle(this),
448+
minWidth = Number.parseFloat(style.minWidth),
449+
minHeight = Number.parseFloat(style.minHeight),
450+
maxWidth = window.innerWidth - left,
451+
maxHeight = window.innerHeight - top;
452+
453+
let width = Number.parseFloat(style.width),
454+
height = Number.parseFloat(style.height);
455+
456+
switch (true) {
457+
case isUpShift(event):
458+
height -= STEP_SIZE;
459+
break;
460+
case isDownShift(event):
461+
height += STEP_SIZE;
462+
break;
463+
case isLeftShift(event):
464+
width -= STEP_SIZE;
465+
break;
466+
case isRightShift(event):
467+
width += STEP_SIZE;
468+
break;
469+
}
470+
471+
width = clamp(width, minWidth, maxWidth);
472+
height = clamp(height, minHeight, maxHeight);
473+
474+
Object.assign(this.style, {
475+
width: `${width}px`,
476+
height: `${height}px`,
477+
});
478+
}
479+
480+
_attachMouseDragHandlers() {
481+
this._detachResizeHandlers();
370482

371483
window.addEventListener("mousemove", this._dragMouseMoveHandler);
372484
window.addEventListener("mouseup", this._dragMouseUpHandler);
373485
}
374486

375-
_detachDragHandlers() {
487+
_detachMouseDragHandlers() {
376488
window.removeEventListener("mousemove", this._dragMouseMoveHandler);
377489
window.removeEventListener("mouseup", this._dragMouseUpHandler);
378490
}
379491

380492
_onResizeMouseDown(event) {
381-
if (!(this.resizable && this.onDesktop)) {
493+
if (!this._movable || !this.resizable) {
382494
return;
383495
}
384496

@@ -409,14 +521,14 @@ class Dialog extends Popup {
409521
left: `${left}px`,
410522
});
411523

412-
this._attachResizeHandlers();
524+
this._attachMouseResizeHandlers();
413525
}
414526

415527
_onResizeMouseMove(event) {
416528
const { clientX, clientY } = event;
417529

418-
let newWidth;
419-
let newLeft;
530+
let newWidth,
531+
newLeft;
420532

421533
if (this._isRTL) {
422534
newWidth = clamp(
@@ -461,19 +573,18 @@ class Dialog extends Popup {
461573
this._minWidth = null;
462574
this._minHeight = null;
463575

464-
this._detachResizeHandlers();
576+
this._detachMouseResizeHandlers();
465577
}
466578

467-
_attachResizeHandlers() {
468-
ResizeHandler.deregister(this, this._screenResizeHandler);
469-
ResizeHandler.deregister(document.body, this._screenResizeHandler);
579+
_attachMouseResizeHandlers() {
580+
this._detachResizeHandlers();
470581

471582
window.addEventListener("mousemove", this._resizeMouseMoveHandler);
472583
window.addEventListener("mouseup", this._resizeMouseUpHandler);
473584
this.addEventListener("ui5-before-close", this._revertSize);
474585
}
475586

476-
_detachResizeHandlers() {
587+
_detachMouseResizeHandlers() {
477588
window.removeEventListener("mousemove", this._resizeMouseMoveHandler);
478589
window.removeEventListener("mouseup", this._resizeMouseUpHandler);
479590
}

packages/main/src/themes/Dialog.css

+5
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@
2828
cursor: auto;
2929
}
3030

31+
.ui5-popup-header-root:focus {
32+
outline: var(--_ui5_dialog_header_focus_width) dotted var(--sapContent_FocusColor);
33+
outline-offset: var(--_ui5_dialog_header_focus_offset);
34+
}
35+
3136
.ui5-popup-root {
3237
display: flex;
3338
flex-direction: column;
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
:root {
22
--_ui5_dialog_resize_handle_color: var(--sapButton_Lite_TextColor);
3+
--_ui5_dialog_header_focus_width: 0.0625rem;
4+
--_ui5_dialog_header_focus_offset: -0.1875rem;
35
}

packages/main/src/themes/sap_belize_hcb/Dialog-parameters.css

+2
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,6 @@
22

33
:root {
44
--_ui5_dialog_resize_handle_color: var(--sapContent_IconColor);
5+
--_ui5_dialog_header_focus_width: 0.125rem;
6+
--_ui5_dialog_header_focus_offset: -0.25rem;
57
}

packages/main/src/themes/sap_belize_hcw/Dialog-parameters.css

+2
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,6 @@
22

33
:root {
44
--_ui5_dialog_resize_handle_color: var(--sapContent_IconColor);
5+
--_ui5_dialog_header_focus_width: 0.125rem;
6+
--_ui5_dialog_header_focus_offset: -0.25rem;
57
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
@import "../base/Dialog-parameters.css";
2+
3+
:root {
4+
--_ui5_dialog_resize_handle_color: var(--sapContent_IconColor);
5+
--_ui5_dialog_header_focus_width: 0.125rem;
6+
--_ui5_dialog_header_focus_offset: -0.25rem;
7+
}

packages/main/src/themes/sap_fiori_3_hcb/parameters-bundle.css

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
@import "../base/ColorPalette-parameters.css";
1010
@import "./DatePicker-parameters.css";
1111
@import "./DayPicker-parameters.css";
12-
@import "../base/Dialog-parameters.css";
12+
@import "./Dialog-parameters.css";
1313
@import "../base/GroupHeaderListItem-parameters.css";
1414
@import "./Input-parameters.css";
1515
@import "./InputIcon-parameters.css";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
@import "../base/Dialog-parameters.css";
2+
3+
:root {
4+
--_ui5_dialog_resize_handle_color: var(--sapContent_IconColor);
5+
--_ui5_dialog_header_focus_width: 0.125rem;
6+
--_ui5_dialog_header_focus_offset: -0.25rem;
7+
}

packages/main/src/themes/sap_fiori_3_hcw/parameters-bundle.css

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
@import "../base/ColorPalette-parameters.css";
1010
@import "./DatePicker-parameters.css";
1111
@import "./DayPicker-parameters.css";
12-
@import "../base/Dialog-parameters.css";
12+
@import "./Dialog-parameters.css";
1313
@import "../base/GroupHeaderListItem-parameters.css";
1414
@import "./Input-parameters.css";
1515
@import "./InputIcon-parameters.css";

0 commit comments

Comments
 (0)