Skip to content

Commit e7c2518

Browse files
authored
feat(ui5-popover): prevent closing when no opener (#1911)
The change is related to the Quick View topic and affects the Input, InputSuggestions and Popover, and enables the following behaviour: (1) open an additional Popover from a suggestion on mouseover and suggestion-item-preview events, (2) to move the focus in that Popover, (3) to keep it open without a visible opener, and (4) the ability to get back via ESC. Add parameter to the openBy method to prevent the popover from closing when its opener is not visible anymore and use the old placement when the opener is gone (by default the popover would go to the top-left most corner). Add public method Popup.prototype.applyFocus (the logic has been extracted from applyInitialFocus) that focuses the first focusable element inside the Popup Change the focus retaining logic in Input/InputSuggestions as follows: returns the focus to the input field only when a suggestion is selected by the user, in other cases - does not, because otherwise focusing another popover would not be possible as the Input keeps getting focus again and again. Previously it was not possible to use the "close" params with the ResponsivePopover, now it forwards them to the Popover. Create a test page to show the entire flow Related to: #1768
1 parent b0551b3 commit e7c2518

File tree

10 files changed

+234
-30
lines changed

10 files changed

+234
-30
lines changed

packages/main/src/Input.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -531,7 +531,9 @@ class Input extends UI5Element {
531531
const shouldOpenSuggestions = this.shouldOpenSuggestions();
532532

533533
this.updateStaticAreaItemContentDensity();
534-
this.Suggestions.toggle(shouldOpenSuggestions);
534+
this.Suggestions.toggle(shouldOpenSuggestions, {
535+
preventFocusRestore: !this.hasSuggestionItemSelected,
536+
});
535537

536538
RenderScheduler.whenFinished().then(async () => {
537539
this._listWidth = await this.Suggestions._getListWidth();

packages/main/src/Popover.js

+18-3
Original file line numberDiff line numberDiff line change
@@ -253,13 +253,16 @@ class Popover extends Popup {
253253
* Opens the popover.
254254
* @param {HTMLElement} opener the element that the popover is opened by
255255
* @param {boolean} preventInitialFocus prevents applying the focus inside the popover
256+
* @param {boolean} closeWithOpener defines if the popover would closes when its opener is no longer visible (true by default)
256257
* @public
257258
*/
258-
openBy(opener, preventInitialFocus = false) {
259+
openBy(opener, preventInitialFocus = false, closeWithOpener = true) {
259260
if (!opener || this.opened) {
260261
return;
261262
}
263+
262264
this._opener = opener;
265+
this._closeWithOpener = closeWithOpener;
263266

264267
super.open(preventInitialFocus);
265268
}
@@ -314,9 +317,17 @@ class Popover extends Popup {
314317
}
315318

316319
show() {
320+
let placement;
317321
const popoverSize = this.popoverSize;
318322
const openerRect = this._opener.getBoundingClientRect();
319-
const placement = this.calcPlacement(openerRect, popoverSize);
323+
324+
if (!this._closeWithOpener && this.shouldCloseDueToNoOpener(openerRect)) {
325+
// use the old placement when the opener is gone
326+
placement = this._oldPlacement;
327+
} else {
328+
placement = this.calcPlacement(openerRect, popoverSize);
329+
}
330+
320331
const stretching = this.horizontalAlign === PopoverHorizontalAlign.Stretch;
321332

322333
if (this._preventRepositionAndClose) {
@@ -396,7 +407,11 @@ class Popover extends Popup {
396407

397408
const placementType = this.getActualPlacementType(targetRect, popoverSize);
398409

399-
this._preventRepositionAndClose = this.shouldCloseDueToNoOpener(targetRect) || this.shouldCloseDueToOverflow(placementType, targetRect);
410+
if (!this._closeWithOpener) {
411+
this._preventRepositionAndClose = this.shouldCloseDueToNoOpener(targetRect) || this.shouldCloseDueToOverflow(placementType, targetRect);
412+
} else {
413+
this._preventRepositionAndClose = false;
414+
}
400415

401416
const isVertical = placementType === PopoverPlacementType.Top
402417
|| placementType === PopoverPlacementType.Bottom;

packages/main/src/Popup.js

+9
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,15 @@ class Popup extends UI5Element {
246246
* @protected
247247
*/
248248
applyInitialFocus() {
249+
this.applyFocus();
250+
}
251+
252+
/**
253+
* Focuses the element denoted by <code>initialFocus</code>, if provided,
254+
* or the first focusable element otherwise.
255+
* @public
256+
*/
257+
applyFocus() {
249258
const element = this.getRootNode().getElementById(this.initialFocus)
250259
|| document.getElementById(this.initialFocus)
251260
|| getFirstFocusableElement(this);

packages/main/src/ResponsivePopover.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -124,9 +124,9 @@ class ResponsivePopover extends Popover {
124124
* Closes the popover/dialog.
125125
* @public
126126
*/
127-
close() {
127+
close(escPressed = false, preventRegistryUpdate = false, preventFocusRestore = false) {
128128
if (!isPhone()) {
129-
super.close();
129+
super.close(escPressed, preventRegistryUpdate, preventFocusRestore);
130130
} else {
131131
this._dialog.close();
132132
}

packages/main/src/features/InputSuggestions.js

+4-4
Original file line numberDiff line numberDiff line change
@@ -92,13 +92,13 @@ class Suggestions {
9292
return false;
9393
}
9494

95-
toggle(bToggle) {
95+
toggle(bToggle, { preventFocusRestore }) {
9696
const toggle = bToggle !== undefined ? bToggle : !this.isOpened();
9797

9898
if (toggle) {
9999
this.open();
100100
} else {
101-
this.close();
101+
this.close(preventFocusRestore);
102102
}
103103
}
104104

@@ -113,9 +113,9 @@ class Suggestions {
113113
this.responsivePopover.open(this._getComponent());
114114
}
115115

116-
async close() {
116+
async close(preventFocusRestore = false) {
117117
this.responsivePopover = await this._respPopover();
118-
this.responsivePopover.close();
118+
this.responsivePopover.close(false, false, preventFocusRestore);
119119
}
120120

121121
updateSelectedItemPosition(pos) {

packages/main/test/pages/Input.html

+55-19
Original file line numberDiff line numberDiff line change
@@ -58,16 +58,27 @@ <h3> Input suggestions with ui5-suggestion-item</h3>
5858
<br><br>
5959

6060
<ui5-input id="inputPreview" show-suggestions style="width: 200px">
61-
<ui5-suggestion-item class="suggestionItem" text="Cozy"></ui5-suggestion-item>
62-
<ui5-suggestion-item class="suggestionItem" text="Compact"></ui5-suggestion-item>
63-
<ui5-suggestion-item class="suggestionItem" text="Condensed"></ui5-suggestion-item>
64-
<ui5-suggestion-item class="suggestionItem" text="Cozy"></ui5-suggestion-item>
65-
<ui5-suggestion-item class="suggestionItem" text="Compact"></ui5-suggestion-item>
66-
<ui5-suggestion-item class="suggestionItem" text="Condensed"></ui5-suggestion-item>
61+
<ui5-suggestion-item class="suggestionItem" text="Laptop Lenovo"></ui5-suggestion-item>
62+
<ui5-suggestion-item class="suggestionItem" text="HP Monitor 24"></ui5-suggestion-item>
63+
<ui5-suggestion-item class="suggestionItem" text="IPhone 6s"></ui5-suggestion-item>
64+
<ui5-suggestion-item class="suggestionItem" text="Dell"></ui5-suggestion-item>
65+
<ui5-suggestion-item class="suggestionItem" text="IPad Air"></ui5-suggestion-item>
6766
</ui5-input>
6867

69-
<ui5-popover id="quickViewCard" header-text="My Heading" id="pop" placement-type="Right">
70-
<ui5-button>Click me</ui5-button>
68+
<ui5-popover id="quickViewCard" no-arrow placement-type="Right" height="500px">
69+
<ui5-input id="searchInput" style="width: 300px">
70+
<ui5-icon id="searchIcon" slot="icon" name="search"></ui5-icon>
71+
</ui5-input>
72+
<ui5-list style="height: 400px">
73+
<ui5-li-groupheader>Actions</ui5-li-groupheader>
74+
<ui5-li>Delete Product</ui5-li>
75+
<ui5-li>Audit Log Settings</ui5-li>
76+
<ui5-li>OData API Audit</ui5-li>
77+
<ui5-li-groupheader>Products</ui5-li-groupheader>
78+
<ui5-li image="./img/HT-1010.jpg" icon="navigation-right-arrow" info="Re-stock" description="#12331232131" info-state="Error">Laptop Lenovo</ui5-li>
79+
<ui5-li image="./img/HT-1022.jpg" icon="navigation-right-arrow" info="Re-stock" description="#12331232131" info-state="Error">IPhone 3</ui5-li>
80+
<ui5-li image="./img/HT-1030.jpg" icon-end icon="navigation-right-arrow" description="#12331232131" info="Reuqired" info-state="Error">HP Monitor 24</ui5-li>
81+
</ui5-list>
7182
</ui5-popover>
7283

7384
<br/>
@@ -406,42 +417,67 @@ <h3>Test ariaLabel and ariaLabelledBy</h3>
406417
inputChangeResult.value = ++inputChangeResultCounter;
407418
});
408419

409-
// Preview suggestion item events
420+
// Quick View Card sample
421+
var focusQuickView = false;
422+
410423
inputPreview.addEventListener("ui5-suggestion-item-preview", function (event) {
411424
var item = event.detail.targetRef;
425+
quickViewCard.close();
426+
quickViewCard.openBy(item, true /* preventInitialFocus */, false /* closeWithOpener */);
427+
428+
// log info
412429
inputItemPreviewRes.value = item.textContent;
413-
414-
quickViewCard.close(false, true, true);
415-
quickViewCard.openBy(item, true);
430+
console.log("suggestion-item-preview", item);
416431
});
417432

418-
inputPreview.addEventListener("keyup", function (event) {
433+
inputPreview.addEventListener("keyup", async function (event) {
419434
const item = event.target.previewItem;
420-
const combination = event.key === "1" && event.shiftKey && event.ctrlKey;
435+
const combination = event.key === "1";
436+
437+
if (combination) {
438+
focusQuickView = true;
439+
await RenderScheduler.whenFinished();
440+
quickViewCard.applyFocus();
441+
console.log("set focus to quickcard");
442+
}
421443

422-
keyupResult.value = combination ? "[ctr+shift+1] on item: " + (item && item.text) : event.key;
444+
// log info
445+
keyupResult.value = event.key + " on item: " + (item && item.text);
446+
console.log("keyup", event.key);
423447
});
424448

425449
[].slice.call(document.querySelectorAll(".suggestionItem")).forEach(function(el) {
426450
el.addEventListener("mouseover", function (event) {
427451
const targetRef = event.detail.targetRef;
452+
quickViewCard.close();
453+
quickViewCard.openBy(item, true /* preventInitialFocus */, false /* closeWithOpener */);
428454

455+
// log info
429456
mouseoverResult.value = targetRef.textContent;
430-
quickViewCard.openBy(targetRef, true);
457+
console.log("mouseover");
431458
});
432459

433460
el.addEventListener("mouseout", function (event) {
434-
const targetRef = event.detail.targetRef;
461+
if (!focusQuickView) {
462+
quickViewCard.close(false, false, true);
463+
}
464+
465+
focusQuickView = false;
435466

436-
mouseoutResult.value = targetRef.textContent;
437-
quickViewCard.close(false, true, true);
467+
// log info
468+
mouseoutResult.value = event.detail.targetRef.textContent;
469+
console.log("mouseout");
438470
});
439471
});
440472

441473
inputPreview.addEventListener("ui5-suggestion-scroll", function (event) {
442474
console.log("scroll", { scrolltop: event.detail.scrollTop });
443475
});
444476

477+
inputPreview.addEventListener("focusin", function (event) {
478+
console.log("focusin");
479+
});
480+
445481
inputPreview.addEventListener("focusout", function (event) {
446482
console.log("focusout");
447483
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
6+
<meta http-equiv="X-UA-Compatible" content="ie=edge">
7+
<title>Input Quick View</title>
8+
<script src="../../webcomponentsjs/webcomponents-loader.js"></script>
9+
<script src="../../resources/bundle.esm.js" type="module"></script>
10+
<script nomodule src="../../resources/bundle.es5.js"></script>
11+
<script>delete Document.prototype.adoptedStyleSheets;</script>
12+
</head>
13+
14+
<body style="background-color: var(--sapBackgroundColor);">
15+
<h1> Quick View sample</h1>
16+
<ul>
17+
<li>hover on item to see quick view</li>
18+
<li>navigate via the arrows to see quick view</li>
19+
<li>press [ctrl + shift + 1] to enter the quick view</li>
20+
</ul>
21+
<ui5-input id="inputPreview" show-suggestions style="width: 200px; margin-top: 50px">
22+
<ui5-suggestion-item class="suggestionItem" text="Laptop Lenovo"></ui5-suggestion-item>
23+
<ui5-suggestion-item class="suggestionItem" text="HP Monitor 24"></ui5-suggestion-item>
24+
<ui5-suggestion-item class="suggestionItem" text="IPhone 6s"></ui5-suggestion-item>
25+
<ui5-suggestion-item class="suggestionItem" text="Dell"></ui5-suggestion-item>
26+
<ui5-suggestion-item class="suggestionItem" text="IPad Air"></ui5-suggestion-item>
27+
</ui5-input>
28+
29+
<ui5-popover id="quickViewCard" no-arrow placement-type="Right" height="500px">
30+
<ui5-input id="searchInput" style="width: 300px">
31+
<ui5-icon id="searchIcon" slot="icon" name="search"></ui5-icon>
32+
</ui5-input>
33+
<ui5-list style="height: 400px">
34+
<ui5-li-groupheader>Actions</ui5-li-groupheader>
35+
<ui5-li>Delete Product</ui5-li>
36+
<ui5-li>Audit Log Settings</ui5-li>
37+
<ui5-li>OData API Audit</ui5-li>
38+
<ui5-li-groupheader>Products</ui5-li-groupheader>
39+
<ui5-li image="./img/HT-1010.jpg" icon="navigation-right-arrow" info="Re-stock" description="#12331232131" info-state="Error">Laptop Lenovo</ui5-li>
40+
<ui5-li image="./img/HT-1022.jpg" icon="navigation-right-arrow" info="Re-stock" description="#12331232131" info-state="Error">IPhone 3</ui5-li>
41+
<ui5-li image="./img/HT-1030.jpg" icon-end icon="navigation-right-arrow" description="#12331232131" info="Reuqired" info-state="Error">HP Monitor 24</ui5-li>
42+
</ui5-list>
43+
</ui5-popover>
44+
45+
<script>
46+
var focusQuickView = false;
47+
48+
/*
49+
* Open quick view on suggestion-item-preview
50+
*/
51+
inputPreview.addEventListener("suggestion-item-preview", function (event) {
52+
const targetRef = event.detail.targetRef;
53+
54+
quickViewCard.close();
55+
quickViewCard.openBy(targetRef, true /* preventInitialFocus */, false /* closeWithOpener */);
56+
});
57+
58+
/*
59+
* Focus quick view on [ctrl + shift + 1]
60+
*/
61+
inputPreview.addEventListener("keyup", async function (event) {
62+
const combination = event.key === "1" && event.ctrlKey && event.shiftKey;
63+
64+
if (combination) {
65+
focusQuickView = true;
66+
await RenderScheduler.whenFinished();
67+
quickViewCard.applyFocus();
68+
}
69+
});
70+
71+
/*
72+
* Toggle quick view on mouseover/mouseout
73+
*/
74+
[].slice.call(document.querySelectorAll(".suggestionItem")).forEach(function(el) {
75+
el.addEventListener("mouseover", function (event) {
76+
const targetRef = event.detail.targetRef;
77+
78+
quickViewCard.close();
79+
quickViewCard.openBy(targetRef, true /* preventInitialFocus */, false /* closeWithOpener */);
80+
});
81+
82+
el.addEventListener("mouseout", function (event) {
83+
// if (!focusQuickView) {
84+
// quickViewCard.close(false, false, true);
85+
// }
86+
87+
// focusQuickView = false;
88+
});
89+
});
90+
</script>
91+
</body>
92+
</html>

packages/main/test/pages/Popover.html

+28
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,11 @@
185185
</div>
186186
</ui5-popover>
187187

188+
<br>
189+
<br>
190+
<ui5-title>Test open popup and hide opener</ui5-title>
191+
<ui5-button id="btnQuickViewCardOpener">Open popup and hide myself</ui5-button>
192+
<ui5-button id="btnMoveFocus">focusable element</ui5-button>
188193
<br>
189194
<br>
190195
<ui5-title>placement-type="Right", but no space on the right</ui5-title>
@@ -292,6 +297,23 @@
292297
</ui5-list>
293298
</ui5-popover>
294299

300+
301+
<ui5-popover id="quickViewCard" placement-type="Right" height="500px">
302+
<ui5-input id="searchInput" style="width: 300px">
303+
<ui5-icon id="searchIcon" slot="icon" name="search"></ui5-icon>
304+
</ui5-input>
305+
<ui5-list style="height: 400px">
306+
<ui5-li-groupheader>Actions</ui5-li-groupheader>
307+
<ui5-li>Delete Product</ui5-li>
308+
<ui5-li>Audit Log Settings</ui5-li>
309+
<ui5-li>OData API Audit</ui5-li>
310+
<ui5-li-groupheader>Products</ui5-li-groupheader>
311+
<ui5-li image="./img/HT-1010.jpg" icon="navigation-right-arrow" info="Re-stock" description="#12331232131" info-state="Error">Laptop Lenovo</ui5-li>
312+
<ui5-li image="./img/HT-1022.jpg" icon="navigation-right-arrow" info="Re-stock" description="#12331232131" info-state="Error">IPhone 3</ui5-li>
313+
<ui5-li image="./img/HT-1030.jpg" icon-end icon="navigation-right-arrow" description="#12331232131" info="Reuqired" info-state="Error">HP Monitor 24</ui5-li>
314+
</ui5-list>
315+
</ui5-popover>
316+
295317
<script>
296318
btn.addEventListener("click", function (event) {
297319
pop.openBy(btn);
@@ -352,6 +374,12 @@
352374
btnFullWidthBottom.addEventListener("click", function (event) {
353375
popFullWidthBottom.openBy(btnFullWidthBottom);
354376
});
377+
378+
btnQuickViewCardOpener.addEventListener("click", function (event) {
379+
quickViewCard.openBy(btnQuickViewCardOpener, true, false);
380+
381+
btnQuickViewCardOpener.setAttribute("hidden", true);
382+
})
355383
</script>
356384
</body>
357385

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ describe("Input general interaction", () => {
136136
inputItemPreview.click();
137137
inputItemPreview.keys("ArrowDown");
138138

139-
assert.strictEqual(inputItemPreviewRes.getValue(), "Cozy", "First item has been previewed");
139+
assert.strictEqual(inputItemPreviewRes.getValue(), "Laptop Lenovo", "First item has been previewed");
140140
});
141141

142142
it("fires suggestion-scroll event", () => {

0 commit comments

Comments
 (0)