chore: fix elementToFocusAfterHide when shadow dom is used
This commit is contained in:
parent
0bc604d600
commit
a96c9692af
2 changed files with 42 additions and 61 deletions
|
|
@ -112,58 +112,6 @@ const supportsCSSTypedObject = window.CSS?.number && document.body.attributeStyl
|
||||||
* configuration, it can be used to build (modal) dialogs, tooltips, dropdowns, popovers,
|
* configuration, it can be used to build (modal) dialogs, tooltips, dropdowns, popovers,
|
||||||
* bottom/top/left/right sheets etc.
|
* bottom/top/left/right sheets etc.
|
||||||
*
|
*
|
||||||
* ### About contentNode, contentWrapperNode and renderTarget.
|
|
||||||
*
|
|
||||||
* #### contentNode
|
|
||||||
* Node containing actual overlay contents.
|
|
||||||
* It will not be touched by the OverlayController, it will only set attributes needed
|
|
||||||
* for accessibility.
|
|
||||||
*
|
|
||||||
* #### contentWrapperNode
|
|
||||||
* The 'positioning' element.
|
|
||||||
* For local overlays, this node will be provided to Popper and all
|
|
||||||
* inline positioning styles will be added here. It will also act as the container of an arrow
|
|
||||||
* element (the arrow needs to be a sibling of contentNode for Popper to work correctly).
|
|
||||||
* When projecting a contentNode from a shadowRoot, it is essential to have the wrapper in
|
|
||||||
* shadow dom, so that contentNode can be styled via `::slotted` from the shadow root.
|
|
||||||
* The Popper arrow can then be styled from that same shadow root as well.
|
|
||||||
* For global overlays, the contentWrapperNode will be appended to the globalRootNode structure.
|
|
||||||
*
|
|
||||||
* #### renderTarget
|
|
||||||
* Usually the parent node of contentWrapperNode that either exists locally or globally.
|
|
||||||
* When a responsive scenario is created (in which we switch from global to local or vice versa)
|
|
||||||
* we need to know where we should reappend contentWrapperNode (or contentNode in case it's projected)
|
|
||||||
*
|
|
||||||
* So a regular flow can be summarized as follows:
|
|
||||||
* 1. Application Developer spawns an OverlayController with a contentNode reference
|
|
||||||
* 2. OverlayController will create a contentWrapperNode around contentNode (or consumes when provided)
|
|
||||||
* 3. contentWrapperNode will be appended to the right renderTarget
|
|
||||||
*
|
|
||||||
* There are subtle differences depending on the following factors:
|
|
||||||
* - whether in global/local placement mode
|
|
||||||
* - whether contentNode projected
|
|
||||||
* - whether an arrow is provided
|
|
||||||
*
|
|
||||||
* This leads to the following possible combinations:
|
|
||||||
* - [l1]. local + no content projection + no arrow
|
|
||||||
* - [l2]. local + content projection + no arrow
|
|
||||||
* - [l3]. local + no content projection + arrow
|
|
||||||
* - [l4]. local + content projection + arrow
|
|
||||||
* - [g1]. global
|
|
||||||
*
|
|
||||||
* #### html structure for a content projected node
|
|
||||||
* <div id="contentWrapperNode">
|
|
||||||
* <slot name="contentNode"></slot>
|
|
||||||
* <div data-popper-arrow></div>
|
|
||||||
* </div>
|
|
||||||
*
|
|
||||||
* Structure above depicts [l4]
|
|
||||||
* So in case of [l1] and [l3], the <slot> element would be a regular element
|
|
||||||
* In case of [l1] and [l2], there would be no arrow.
|
|
||||||
* Note that a contentWrapperNode should be provided for [l2], [l3] and [l4]
|
|
||||||
* In case of a global overlay ([g1]), it's enough to provide just the contentNode.
|
|
||||||
* In case of a local overlay or a responsive overlay switching from placementMode, one should
|
|
||||||
* always configure as if it were a local overlay.
|
|
||||||
*/
|
*/
|
||||||
export class OverlayController extends EventTargetShim {
|
export class OverlayController extends EventTargetShim {
|
||||||
/**
|
/**
|
||||||
|
|
@ -944,15 +892,19 @@ export class OverlayController extends EventTargetShim {
|
||||||
|
|
||||||
/** @protected */
|
/** @protected */
|
||||||
_restoreFocus() {
|
_restoreFocus() {
|
||||||
const { activeElement } = /** @type {ShadowRoot} */ (this.contentWrapperNode.getRootNode());
|
// We only are allowed to move focus if we (still) 'own' the active element.
|
||||||
// We only are allowed to move focus if we (still) 'own' it.
|
// Otherwise we assume the 'outside world' has purposefully taken over
|
||||||
// Otherwise we assume the 'outside world' has, purposefully, taken over
|
const { activeElement } = /** @type {ShadowRoot} */ (this.contentNode.getRootNode());
|
||||||
if (activeElement instanceof HTMLElement && this.contentWrapperNode.contains(activeElement)) {
|
const weStillOwnActiveElement =
|
||||||
if (this.elementToFocusAfterHide) {
|
activeElement instanceof HTMLElement && this.contentNode.contains(activeElement);
|
||||||
this.elementToFocusAfterHide.focus();
|
if (!weStillOwnActiveElement) {
|
||||||
} else {
|
return;
|
||||||
activeElement.blur();
|
}
|
||||||
}
|
|
||||||
|
if (this.elementToFocusAfterHide) {
|
||||||
|
this.elementToFocusAfterHide.focus();
|
||||||
|
} else {
|
||||||
|
activeElement.blur();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -889,6 +889,35 @@ describe('OverlayController', () => {
|
||||||
expect(document.activeElement).to.equal(input);
|
expect(document.activeElement).to.equal(input);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('supports elementToFocusAfterHide option when shadowRoot involved involved', async () => {
|
||||||
|
const input = /** @type {HTMLElement} */ (await fixture('<input />'));
|
||||||
|
const contentNode = /** @type {HTMLElement} */ (
|
||||||
|
await fixture('<div><textarea></textarea></div>')
|
||||||
|
);
|
||||||
|
|
||||||
|
const shadowEl = document.createElement('div');
|
||||||
|
shadowEl.attachShadow({ mode: 'open' });
|
||||||
|
/** @type {ShadowRoot} */ (shadowEl.shadowRoot).innerHTML = `<slot></slot>`;
|
||||||
|
shadowEl.appendChild(contentNode);
|
||||||
|
document.body.appendChild(shadowEl);
|
||||||
|
|
||||||
|
const ctrl = new OverlayController({
|
||||||
|
...withGlobalTestConfig(),
|
||||||
|
elementToFocusAfterHide: input,
|
||||||
|
contentNode,
|
||||||
|
});
|
||||||
|
|
||||||
|
await ctrl.show();
|
||||||
|
const textarea = /** @type {HTMLTextAreaElement} */ (contentNode.querySelector('textarea'));
|
||||||
|
textarea.focus();
|
||||||
|
expect(document.activeElement).to.equal(textarea);
|
||||||
|
|
||||||
|
await ctrl.hide();
|
||||||
|
expect(document.activeElement).to.equal(input);
|
||||||
|
|
||||||
|
document.body.removeChild(shadowEl);
|
||||||
|
});
|
||||||
|
|
||||||
it(`only sets focus when outside world didn't take over already`, async () => {
|
it(`only sets focus when outside world didn't take over already`, async () => {
|
||||||
const input = /** @type {HTMLElement} */ (await fixture('<input />'));
|
const input = /** @type {HTMLElement} */ (await fixture('<input />'));
|
||||||
const outsideButton = /** @type {HTMLButtonElement} */ (await fixture('<button></button>'));
|
const outsideButton = /** @type {HTMLButtonElement} */ (await fixture('<button></button>'));
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue