Skip to content

Commit 45cdcc2

Browse files
authored
refactor(ui5-popover): Keep popup open if focus is inside (#1937)
Another PR to address the "QuickCardView" topic. The change keeps the Popover open, without an opener, if the focus is inside that popover. add prevent-focus-restore protected property in addition to the preventFocusRestore param of Popup.prototype.close method, because the quickCardView might close due to user interaction - click, TAB, ESC. remove the recently added _closeWithOpener param, as now the popover without an opener would remain open if the focus is inside it and will be closed otherwise. QuickCardView Interaction (test it on http://localhost:8080/test-resources/pages/Input_quickview.html) Once opened, both Suggestions Popover and QuickCardView will close if: neither the SearchField nor the QuickCardView has the focus, because the user clicks somewhere outside or the user uses the TAB | SHIFT + TAB and the focus is somewhere else Once opened, the QuickCardView remains open and Suggestions Popover closes if: the focus moves to the QuickCardView, because the Popover.prototype.applyFocus method is called, the user clicks inside the QuickCardView or the user uses the TAB | SHIFT + TAB key and the focus is goes inside the QuickCardView Closes: #1768
1 parent 0934f0c commit 45cdcc2

File tree

11 files changed

+137
-54
lines changed

11 files changed

+137
-54
lines changed

packages/main/src/Input.js

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -628,7 +628,7 @@ class Input extends UI5Element {
628628
}
629629

630630
if (this.popover) {
631-
this.popover.close(false, false, true);
631+
this.popover.close();
632632
}
633633

634634
this.previousValue = "";
@@ -898,13 +898,19 @@ class Input extends UI5Element {
898898
onItemMouseOver(event) {
899899
const item = event.target;
900900
const suggestion = this.getSuggestionByListItem(item);
901-
suggestion && suggestion.fireEvent("mouseover", { targetRef: item });
901+
suggestion && suggestion.fireEvent("mouseover", {
902+
item: suggestion,
903+
targetRef: item,
904+
});
902905
}
903906

904907
onItemMouseOut(event) {
905908
const item = event.target;
906909
const suggestion = this.getSuggestionByListItem(item);
907-
suggestion && suggestion.fireEvent("mouseout", { targetRef: item });
910+
suggestion && suggestion.fireEvent("mouseout", {
911+
item: suggestion,
912+
targetRef: item,
913+
});
908914
}
909915

910916
onItemSelected(item, keyboardUsed) {

packages/main/src/InputPopover.hbs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
<ui5-popover
6666
skip-registry-update
6767
_disable-initial-focus
68+
prevent-focus-restore
6869
no-padding
6970
no-arrow
7071
class="ui5-valuestatemessage-popover"

packages/main/src/Popover.js

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -253,16 +253,14 @@ 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)
257256
* @public
258257
*/
259-
openBy(opener, preventInitialFocus = false, closeWithOpener = true) {
258+
openBy(opener, preventInitialFocus = false) {
260259
if (!opener || this.opened) {
261260
return;
262261
}
263262

264263
this._opener = opener;
265-
this._closeWithOpener = closeWithOpener;
266264

267265
super.open(preventInitialFocus);
268266
}
@@ -321,8 +319,9 @@ class Popover extends Popup {
321319
const popoverSize = this.popoverSize;
322320
const openerRect = this._opener.getBoundingClientRect();
323321

324-
if (!this._closeWithOpener && this.shouldCloseDueToNoOpener(openerRect)) {
325-
// use the old placement when the opener is gone
322+
if (this.shouldCloseDueToNoOpener(openerRect) && this.isFocusWithin()) {
323+
// reuse the old placement as the opener is not available,
324+
// but keep the popover open as the focus is within
326325
placement = this._oldPlacement;
327326
} else {
328327
placement = this.calcPlacement(openerRect, popoverSize);
@@ -407,11 +406,7 @@ class Popover extends Popup {
407406

408407
const placementType = this.getActualPlacementType(targetRect, popoverSize);
409408

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

416411
const isVertical = placementType === PopoverPlacementType.Top
417412
|| placementType === PopoverPlacementType.Bottom;

packages/main/src/Popup.js

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { getFirstFocusableElement, getLastFocusableElement } from "@ui5/webcompo
44
import createStyleInHead from "@ui5/webcomponents-base/dist/util/createStyleInHead.js";
55
import PopupTemplate from "./generated/templates/PopupTemplate.lit.js";
66
import PopupBlockLayer from "./generated/templates/PopupBlockLayerTemplate.lit.js";
7-
import { getNextZIndex, getFocusedElement } from "./popup-utils/PopupUtils.js";
7+
import { getNextZIndex, getFocusedElement, isFocusedElementWithinNode } from "./popup-utils/PopupUtils.js";
88
import { addOpenedPopup, removeOpenedPopup } from "./popup-utils/OpenedPopupsRegistry.js";
99

1010
// Styles
@@ -40,9 +40,23 @@ const metadata = {
4040
type: String,
4141
},
4242

43+
/**
44+
* Defines if the focus should be returned to the previously focused element,
45+
* when the popup closes.
46+
* @type {boolean}
47+
* @defaultvalue false
48+
* @public
49+
* @since 1.0.0-rc.8
50+
*/
51+
preventFocusRestore: {
52+
type: Boolean,
53+
},
54+
4355
/**
4456
* Indicates if the elements is open
4557
* @private
58+
* @type {boolean}
59+
* @defaultvalue false
4660
*/
4761
opened: {
4862
type: Boolean,
@@ -273,6 +287,10 @@ class Popup extends UI5Element {
273287
return this.opened;
274288
}
275289

290+
isFocusWithin() {
291+
return isFocusedElementWithinNode(this.shadowRoot.querySelector(".ui5-popup-root"));
292+
}
293+
276294
/**
277295
* Shows the block layer (for modal popups only) and sets the correct z-index for the purpose of popup stacking
278296
* @param {boolean} preventInitialFocus prevents applying the focus inside the popup
@@ -333,7 +351,7 @@ class Popup extends UI5Element {
333351
this._removeOpenedPopup();
334352
}
335353

336-
if (!preventFocusRestore) {
354+
if (!this.preventFocusRestore && !preventFocusRestore) {
337355
this.resetFocus();
338356
}
339357

packages/main/src/TextArea.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -449,7 +449,7 @@ class TextArea extends UI5Element {
449449

450450
async closePopover() {
451451
this.popover = await this._getPopover();
452-
this.popover && this.popover.close(false, false, true);
452+
this.popover && this.popover.close();
453453
}
454454

455455
async _getPopover() {

packages/main/src/TextAreaPopover.hbs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{{#if displayValueStateMessagePopover}}
22
<ui5-popover
33
skip-registry-update
4+
prevent-focus-restore
45
no-padding
56
no-arrow
67
_disable-initial-focus

packages/main/src/popup-utils/OpenedPopupsRegistry.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ const getOpenedPopups = () => {
3030
};
3131

3232
const _keydownListener = event => {
33+
if (!openedRegistry.length) {
34+
return;
35+
}
36+
3337
if (isEscape(event)) {
3438
openedRegistry.pop().instance.close(true);
3539
}

packages/main/src/popup-utils/PopupUtils.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,34 @@ const getFocusedElement = () => {
1010
return (element && typeof element.focus === "function") ? element : null;
1111
};
1212

13+
const isFocusedElementWithinNode = node => {
14+
const fe = getFocusedElement();
15+
16+
if (fe) {
17+
return isNodeContainedWithin(node, fe);
18+
}
19+
20+
return false;
21+
};
22+
23+
const isNodeContainedWithin = (parent, child) => {
24+
let currentNode = parent;
25+
26+
if (currentNode.shadowRoot) {
27+
currentNode = Array.from(currentNode.shadowRoot.children).find(n => n.localName !== "style");
28+
}
29+
30+
if (currentNode === child) {
31+
return true;
32+
}
33+
34+
const childNodes = currentNode.localName === "slot" ? currentNode.assignedNodes() : currentNode.children;
35+
36+
if (childNodes) {
37+
return Array.from(childNodes).some(n => isNodeContainedWithin(n, child));
38+
}
39+
};
40+
1341
const isPointInRect = (x, y, rect) => {
1442
return x >= rect.left && x <= rect.right
1543
&& y >= rect.top && y <= rect.bottom;
@@ -52,4 +80,5 @@ export {
5280
isClickInRect,
5381
getClosedPopupParent,
5482
getNextZIndex,
83+
isFocusedElementWithinNode,
5584
};

packages/main/test/pages/Input.html

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -422,7 +422,7 @@ <h3>Test ariaLabel and ariaLabelledBy</h3>
422422
inputPreview.addEventListener("ui5-suggestion-item-preview", function (event) {
423423
var item = event.detail.targetRef;
424424
quickViewCard.close();
425-
quickViewCard.openBy(item, true /* preventInitialFocus */, false /* closeWithOpener */);
425+
quickViewCard.openBy(item, true /* preventInitialFocus */);
426426

427427
// log info
428428
inputItemPreviewRes.value = item.textContent;
@@ -449,36 +449,28 @@ <h3>Test ariaLabel and ariaLabelledBy</h3>
449449
el.addEventListener("mouseover", function (event) {
450450
const targetRef = event.detail.targetRef;
451451
quickViewCard.close();
452-
quickViewCard.openBy(targetRef, true /* preventInitialFocus */, false /* closeWithOpener */);
452+
quickViewCard.openBy(targetRef, true /* preventInitialFocus */);
453453

454454
// log info
455455
mouseoverResult.value = targetRef.textContent;
456456
console.log("mouseover");
457457
});
458458

459459
el.addEventListener("mouseout", function (event) {
460-
// if (!focusQuickView) {
461-
// quickViewCard.close(false, false, true);
462-
// }
463-
464-
// focusQuickView = false;
465-
466-
// // log info
467-
// mouseoutResult.value = event.detail.targetRef.textContent;
468-
// console.log("mouseout");
469460
});
470461
});
471462

472463
inputPreview.addEventListener("ui5-suggestion-scroll", function (event) {
473464
console.log("scroll", { scrolltop: event.detail.scrollTop });
474465
});
475466

476-
inputPreview.addEventListener("focusin", function (event) {
477-
console.log("focusin");
478-
});
467+
quickViewCard.addEventListener("ui5-before-close", async event => {
468+
const esc = event.detail.escPressed;
479469

480-
inputPreview.addEventListener("focusout", function (event) {
481-
console.log("focusout");
470+
if (esc) {
471+
await RenderScheduler.whenFinished();
472+
inputPreview.focus();
473+
}
482474
});
483475

484476
scrollInput.addEventListener("ui5-suggestion-scroll", function (event) {

packages/main/test/pages/Input_quickview.html

Lines changed: 41 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,23 @@ <h1> Quick View sample</h1>
1818
<li>navigate via the arrows to see quick view</li>
1919
<li>press [ctrl + shift + 1] to enter the quick view</li>
2020
</ul>
21-
<ui5-input id="inputPreview" show-suggestions style="width: 200px; margin-top: 50px">
21+
22+
<div style="display: flex; align-items: center">
23+
<ui5-label>focusable element: </ui5-label><ui5-button>before</ui5-button>
24+
</div>
25+
<br>
26+
<br>
27+
28+
<ui5-input id="inputPreview" show-suggestions style="width: 200px;">
2229
<ui5-suggestion-item class="suggestionItem" text="Laptop Lenovo"></ui5-suggestion-item>
2330
<ui5-suggestion-item class="suggestionItem" text="HP Monitor 24"></ui5-suggestion-item>
2431
<ui5-suggestion-item class="suggestionItem" text="IPhone 6s"></ui5-suggestion-item>
2532
<ui5-suggestion-item class="suggestionItem" text="Dell"></ui5-suggestion-item>
2633
<ui5-suggestion-item class="suggestionItem" text="IPad Air"></ui5-suggestion-item>
2734
</ui5-input>
2835

29-
<ui5-popover id="quickViewCard" no-arrow placement-type="Right" height="500px">
36+
<ui5-popover id="quickViewCard" no-arrow placement-type="Right" height="500px" prevent-focus-restore>
37+
<button>hello</button>
3038
<ui5-input id="searchInput" style="width: 300px">
3139
<ui5-icon id="searchIcon" slot="icon" name="search"></ui5-icon>
3240
</ui5-input>
@@ -42,23 +50,43 @@ <h1> Quick View sample</h1>
4250
</ui5-list>
4351
</ui5-popover>
4452

53+
<br>
54+
<br>
55+
<div style="display: flex; align-items: center">
56+
<ui5-label>focusable element: </ui5-label><ui5-button>after</ui5-button>
57+
</div>
4558
<script>
4659
var focusQuickView = false;
4760

4861
/*
49-
* Open quick view on suggestion-item-preview
62+
* Open quickviewCard on suggestion-item-preview
5063
*/
5164
inputPreview.addEventListener("suggestion-item-preview", function (event) {
5265
const targetRef = event.detail.targetRef;
5366

5467
quickViewCard.close();
55-
quickViewCard.openBy(targetRef, true /* preventInitialFocus */, false /* closeWithOpener */);
68+
quickViewCard.openBy(targetRef, true /* preventInitialFocus */);
69+
});
70+
71+
/*
72+
* Toggle quickviewCard 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 */);
80+
});
81+
82+
el.addEventListener("mouseout", function (event) {
83+
});
5684
});
5785

5886
/*
59-
* Focus quick view on [ctrl + shift + 1]
87+
* Focus quickviewCard on [ctrl + shift + 1]
6088
*/
61-
inputPreview.addEventListener("keyup", async function (event) {
89+
inputPreview.addEventListener("keyup", async event => {
6290
const combination = event.key === "1" && event.ctrlKey && event.shiftKey;
6391

6492
if (combination) {
@@ -69,23 +97,15 @@ <h1> Quick View sample</h1>
6997
});
7098

7199
/*
72-
* Toggle quick view on mouseover/mouseout
100+
* Restore the focus to the input on ESC
73101
*/
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-
// }
102+
quickViewCard.addEventListener("before-close", async event => {
103+
const esc = event.detail.escPressed;
86104

87-
// focusQuickView = false;
88-
});
105+
if (esc) {
106+
await RenderScheduler.whenFinished();
107+
inputPreview.focus();
108+
}
89109
});
90110
</script>
91111
</body>

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

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,11 +132,28 @@ describe("Input general interaction", () => {
132132
it("fires suggestion-item-preview", () => {
133133
const inputItemPreview = $("#inputPreview").shadow$("input");
134134
const inputItemPreviewRes = $("#inputItemPreviewRes");
135+
const EXPECTED_PREVIEW_ITEM_TEXT = "Laptop Lenovo";
135136

137+
// act
136138
inputItemPreview.click();
137139
inputItemPreview.keys("ArrowDown");
140+
141+
// assert
142+
const staticAreaItemClassName = browser.getStaticAreaItemClassName("#inputPreview");
143+
const inputPopover = browser.$(`.${staticAreaItemClassName}`).shadow$("ui5-responsive-popover");
144+
const helpPopover = browser.$("#quickViewCard");
145+
146+
assert.strictEqual(inputItemPreviewRes.getValue(), EXPECTED_PREVIEW_ITEM_TEXT, "First item has been previewed");
147+
assert.ok(helpPopover.isDisplayedInViewport(), "The help popover is open.");
148+
assert.ok(inputPopover.isDisplayedInViewport(), "The input popover is open.");
138149

139-
assert.strictEqual(inputItemPreviewRes.getValue(), "Laptop Lenovo", "First item has been previewed");
150+
// act
151+
const inputInHelpPopover = browser.$("#searchInput").shadow$("input");
152+
inputInHelpPopover.click();
153+
154+
// assert
155+
assert.notOk(inputPopover.isDisplayedInViewport(), "The inpuit popover is closed as it lost the focus.");
156+
assert.ok(helpPopover.isDisplayedInViewport(), "The help popover remains open as the focus is within.");
140157
});
141158

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

0 commit comments

Comments
 (0)