feat: use dialog element for top layer functionality of all overlays
This commit is contained in:
parent
00063d7350
commit
64c0e26c20
33 changed files with 1595 additions and 974 deletions
28
.changeset/twelve-snakes-wait.md
Normal file
28
.changeset/twelve-snakes-wait.md
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
---
|
||||||
|
'@lion/ui': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Overlay System uses `<dialog>` for top layer functionality of all overlays.
|
||||||
|
This means overlays positioned relative to viewport won't be moved to the body.
|
||||||
|
|
||||||
|
This has many benefits for the App Developer:
|
||||||
|
|
||||||
|
- "context" will be kept:
|
||||||
|
- css variables and parts/theme will work
|
||||||
|
- events work without the need for "repropagation" (from body to original context)
|
||||||
|
- accessibility relations between overlay content and its context do not get lost
|
||||||
|
- initial renderings become more predictable (since we don't need multiple initializations on multiple connectedCallbacks)
|
||||||
|
- performance: less initialization, thus better performance
|
||||||
|
- maintainability: handling all edge cases involved in moving an overlay to the body grew out of hand
|
||||||
|
- developer experience:
|
||||||
|
- no extra container components like overlay-frame/calendar-frame needed that maintain styles
|
||||||
|
- adding a contentWrapperNode is not needed anymore
|
||||||
|
|
||||||
|
There could be small differences in timings though (usually we're done rendering quicker now).
|
||||||
|
Code that relies on side effects could be affected. Like:
|
||||||
|
|
||||||
|
- the existence of a global Root node)
|
||||||
|
- the fact that global styles would reach a dialog placed in the body
|
||||||
|
|
||||||
|
For most users using either OverlayController, OverlayMixin or an element that uses OverlayMixin (like LionInputDatepicker, LionRichSelect etc. etc.)
|
||||||
|
nothing will change in the public api.
|
||||||
|
|
@ -76,9 +76,9 @@ export const closeOverlayFromComponent = () => html`
|
||||||
```js preview-story
|
```js preview-story
|
||||||
export const placementOverrides = () => {
|
export const placementOverrides = () => {
|
||||||
const dialog = placement => html`
|
const dialog = placement => html`
|
||||||
<lion-dialog .config=${{ viewportConfig: { placement } }}>
|
<lion-dialog .config="${{ viewportConfig: { placement } }}">
|
||||||
<button slot="invoker">Dialog ${placement}</button>
|
<button slot="invoker">Dialog ${placement}</button>
|
||||||
<div slot="content" class="dialog">
|
<div slot="content" class="dialog demo-box">
|
||||||
Hello! You can close this notification here:
|
Hello! You can close this notification here:
|
||||||
<button
|
<button
|
||||||
class="close-button"
|
class="close-button"
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
/* eslint-disable import/no-extraneous-dependencies */
|
||||||
import { css } from 'lit';
|
import { css } from 'lit';
|
||||||
|
|
||||||
const applyDemoOverlayStyles = () => {
|
const applyDemoOverlayStyles = () => {
|
||||||
|
|
@ -1,21 +1,19 @@
|
||||||
|
/* eslint-disable import/no-extraneous-dependencies */
|
||||||
import { css, LitElement } from 'lit';
|
import { css, LitElement } from 'lit';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {import('../types/OverlayConfig').OverlayConfig} OverlayConfig
|
* @typedef {import('@lion/ui/types/overlays.js').OverlayConfig} OverlayConfig
|
||||||
*/
|
*/
|
||||||
class DemoOverlayBackdrop extends LitElement {
|
class DemoOverlayBackdrop extends LitElement {
|
||||||
static get styles() {
|
static get styles() {
|
||||||
return css`
|
return css`
|
||||||
:host {
|
:host {
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
z-index: 1;
|
|
||||||
background-color: grey;
|
background-color: grey;
|
||||||
opacity: 0.3;
|
opacity: 0.3;
|
||||||
display: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:host(.local-overlays__backdrop--visible) {
|
:host(.local-overlays__backdrop--visible) {
|
||||||
|
|
@ -0,0 +1,538 @@
|
||||||
|
/* eslint-disable max-classes-per-file */
|
||||||
|
/* eslint-disable import/no-extraneous-dependencies */
|
||||||
|
import { html, LitElement, css } from 'lit';
|
||||||
|
import { ref, createRef } from 'lit/directives/ref.js';
|
||||||
|
import { OverlayMixin } from '@lion/ui/overlays.js';
|
||||||
|
import './demo-overlay-system.mjs';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {import('@lion/ui/types/overlays.js').OverlayConfig} OverlayConfig
|
||||||
|
* @typedef {import('@lion/ui/types/overlays.js').OverlayHost} OverlayHost
|
||||||
|
* @typedef {import('@lion/ui/button.js').LionButton} LionButton
|
||||||
|
* @typedef {'top-start'|'top'|'top-end'|'right-start'|'right'|'right-end'|'bottom-start'|'bottom'|'bottom-end'|'left-start'|'left'|'left-end'} LocalPlacement
|
||||||
|
* @typedef {'center'|'top-left'|'top'|'top-right'|'right'|'bottom-right'|'bottom'|'bottom-left'|'left'} ViewportPlacement
|
||||||
|
* @typedef {{refs: { contentNodeWrapper:{ref: {value:HTMLElement}}; closeButton: {ref: {value:HTMLButtonElement|LionButton}; label:string} }}} TemplateDataForOverlay
|
||||||
|
*/
|
||||||
|
class DemoOverlayEl extends OverlayMixin(LitElement) {
|
||||||
|
/** @type {any} */
|
||||||
|
static get properties() {
|
||||||
|
return {
|
||||||
|
simulateViewport: { type: Boolean, attribute: 'simulate-viewport', reflect: true },
|
||||||
|
noDialogEl: { type: Boolean, attribute: 'no-dialog-el' },
|
||||||
|
useAbsolute: { type: Boolean, attribute: 'use-absolute', reflect: true },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles() {
|
||||||
|
return [
|
||||||
|
super.styles || [],
|
||||||
|
css`
|
||||||
|
:host([use-absolute]) dialog {
|
||||||
|
position: absolute !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host([simulate-viewport]) {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host([simulate-viewport]) dialog {
|
||||||
|
position: absolute !important;
|
||||||
|
inset: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host([simulate-viewport])
|
||||||
|
#overlay-content-node-wrapper.global-overlays__overlay-container {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*=== demo invoker and content ===*/
|
||||||
|
|
||||||
|
:host ::slotted([slot='invoker']) {
|
||||||
|
border: 4px dashed;
|
||||||
|
height: 24px;
|
||||||
|
min-width: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host ::slotted([slot='content']) {
|
||||||
|
background-color: black;
|
||||||
|
color: white;
|
||||||
|
height: 54px;
|
||||||
|
min-width: 54px;
|
||||||
|
display: flex;
|
||||||
|
place-items: center;
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.simulateViewport = false;
|
||||||
|
this._noDialogEl = false;
|
||||||
|
this.useAbsolute = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
_defineOverlayConfig() {
|
||||||
|
return /** @type {OverlayConfig} */ ({
|
||||||
|
placementMode: 'local',
|
||||||
|
noDialogEl: this._noDialogEl,
|
||||||
|
popperConfig: { strategy: this.useAbsolute ? 'absolute' : 'fixed' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_setupOpenCloseListeners() {
|
||||||
|
super._setupOpenCloseListeners();
|
||||||
|
|
||||||
|
if (this._overlayInvokerNode) {
|
||||||
|
this._overlayInvokerNode.addEventListener('click', this.toggle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_teardownOpenCloseListeners() {
|
||||||
|
super._teardownOpenCloseListeners();
|
||||||
|
|
||||||
|
if (this._overlayInvokerNode) {
|
||||||
|
this._overlayInvokerNode.removeEventListener('click', this.toggle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
refs = {
|
||||||
|
invokerSlot: /** @type {{value: HTMLSlotElement}} */ (createRef()),
|
||||||
|
backdropSlot: /** @type {{value: HTMLSlotElement}} */ (createRef()),
|
||||||
|
contentSlot: /** @type {{value: HTMLSlotElement}} */ (createRef()),
|
||||||
|
closeButton: /** @type {{value: HTMLButtonElement|LionButton}} */ (createRef()),
|
||||||
|
contentNodeWrapper: /** @type {{value: HTMLElement}} */ (createRef()),
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @overridable
|
||||||
|
* @type {TemplateDataForOverlay}
|
||||||
|
*/
|
||||||
|
get _templateData() {
|
||||||
|
return {
|
||||||
|
refs: {
|
||||||
|
contentNodeWrapper: {
|
||||||
|
ref: this.refs.contentNodeWrapper,
|
||||||
|
},
|
||||||
|
closeButton: {
|
||||||
|
ref: this.refs.closeButton,
|
||||||
|
label: 'close dialog',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static templates = {
|
||||||
|
main: (/** @type {TemplateDataForOverlay} */ { refs }) => html`
|
||||||
|
<slot name="invoker"></slot>
|
||||||
|
<slot name="backdrop"></slot>
|
||||||
|
<div ${ref(refs.contentNodeWrapper.ref)} id="overlay-content-node-wrapper">
|
||||||
|
<slot name="content"></slot>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const ctor = /** @type {typeof DemoOverlayEl} */ (this.constructor);
|
||||||
|
const templates = this.templates || ctor.templates;
|
||||||
|
return templates.main(this._templateData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
customElements.define('demo-overlay-el', DemoOverlayEl);
|
||||||
|
|
||||||
|
class DemoOverlayPositioning extends LitElement {
|
||||||
|
static properties = {
|
||||||
|
placementMode: { attribute: 'placement-mode', type: String },
|
||||||
|
simulateViewport: { type: Boolean, attribute: 'simulate-viewport', reflect: true },
|
||||||
|
_activePos: { type: String, reflect: true, attribute: 'active-pos' },
|
||||||
|
_activeConfig: { type: Object, state: true },
|
||||||
|
};
|
||||||
|
|
||||||
|
static get styles() {
|
||||||
|
return [
|
||||||
|
css`
|
||||||
|
/*=== .pos-container ===*/
|
||||||
|
|
||||||
|
.pos-container {
|
||||||
|
padding: 0.5rem;
|
||||||
|
overflow: hidden;
|
||||||
|
place-items: center;
|
||||||
|
height: 20rem;
|
||||||
|
display: grid;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*=== .pos-btn-wrapper ===*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We need a wrapper for position transforms, so that we can apply scale transforms on .pos-btn hover
|
||||||
|
*/
|
||||||
|
|
||||||
|
.pos-btn-wrapper {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pos-container--local .pos-btn-wrapper--bottom-start,
|
||||||
|
.pos-container--local .pos-btn-wrapper--bottom-end,
|
||||||
|
.pos-container--local .pos-btn-wrapper--top-start,
|
||||||
|
.pos-container--local .pos-btn-wrapper--top-end,
|
||||||
|
.pos-container--local .pos-btn-wrapper--top,
|
||||||
|
.pos-container--local .pos-btn-wrapper--bottom {
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pos-container--local .pos-btn-wrapper--top-start,
|
||||||
|
.pos-container--local .pos-btn-wrapper--top-end,
|
||||||
|
.pos-container--local .pos-btn-wrapper--top {
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pos-container--local .pos-btn-wrapper--bottom-start,
|
||||||
|
.pos-container--local .pos-btn-wrapper--bottom-end,
|
||||||
|
.pos-container--local .pos-btn-wrapper--bottom {
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pos-container--local .pos-btn-wrapper--left-start,
|
||||||
|
.pos-container--local .pos-btn-wrapper--left-end,
|
||||||
|
.pos-container--local .pos-btn-wrapper--right-start,
|
||||||
|
.pos-container--local .pos-btn-wrapper--right-end,
|
||||||
|
.pos-container--local .pos-btn-wrapper--left,
|
||||||
|
.pos-container--local .pos-btn-wrapper--right {
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pos-container--local .pos-btn-wrapper--left-start,
|
||||||
|
.pos-container--local .pos-btn-wrapper--left-end,
|
||||||
|
.pos-container--local .pos-btn-wrapper--left {
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pos-container--local .pos-btn-wrapper--right-start,
|
||||||
|
.pos-container--local .pos-btn-wrapper--right-end,
|
||||||
|
.pos-container--local .pos-btn-wrapper--right {
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pos-container--local .pos-btn-wrapper--bottom-start,
|
||||||
|
.pos-container--local .pos-btn-wrapper--top-start {
|
||||||
|
transform: translateX(-50%) translateX(-48px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pos-container--local .pos-btn-wrapper--bottom-end,
|
||||||
|
.pos-container--local .pos-btn-wrapper--top-end {
|
||||||
|
transform: translateX(-50%) translateX(48px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pos-container--local .pos-btn-wrapper--left-start,
|
||||||
|
.pos-container--local .pos-btn-wrapper--right-start {
|
||||||
|
transform: translateY(calc(-50% - 48px));
|
||||||
|
}
|
||||||
|
|
||||||
|
.pos-container--local .pos-btn-wrapper--left-end,
|
||||||
|
.pos-container--local .pos-btn-wrapper--right-end {
|
||||||
|
transform: translateY(calc(-50% + 48px));
|
||||||
|
}
|
||||||
|
|
||||||
|
.pos-container--global .pos-btn-wrapper {
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pos-container--global .pos-btn-wrapper--center {
|
||||||
|
transform: translateY(-50%) translateX(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pos-container--global .pos-btn-wrapper--top-left {
|
||||||
|
transform: translateY(-50%) translateX(-50%) translateY(-48px) translateX(-48px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pos-container--global .pos-btn-wrapper--top {
|
||||||
|
transform: translateY(-50%) translateX(-50%) translateY(-48px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pos-container--global .pos-btn-wrapper--top-right {
|
||||||
|
transform: translateY(-50%) translateX(-50%) translateY(-48px) translateX(48px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pos-container--global .pos-btn-wrapper--bottom-left {
|
||||||
|
transform: translateY(-50%) translateX(-50%) translateY(48px) translateX(-48px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pos-container--global .pos-btn-wrapper--bottom {
|
||||||
|
transform: translateY(-50%) translateX(-50%) translateY(48px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pos-container--global .pos-btn-wrapper--bottom-right {
|
||||||
|
transform: translateY(-50%) translateX(-50%) translateY(48px) translateX(48px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pos-container--global .pos-btn-wrapper--right {
|
||||||
|
transform: translateY(-50%) translateX(-50%) translateX(48px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pos-container--global .pos-btn-wrapper--left {
|
||||||
|
transform: translateY(-50%) translateX(-50%) translateX(-48px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*=== .pos-btn ===*/
|
||||||
|
|
||||||
|
.pos-btn {
|
||||||
|
padding: 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
-webkit-appearance: button;
|
||||||
|
background-color: transparent;
|
||||||
|
background-image: none;
|
||||||
|
text-transform: none;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 100%;
|
||||||
|
line-height: inherit;
|
||||||
|
color: inherit;
|
||||||
|
margin: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: 0 solid #bfc3d9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pos-btn__inner {
|
||||||
|
border-style: solid;
|
||||||
|
border-width: 2px;
|
||||||
|
border-radius: 100%;
|
||||||
|
width: 0.25rem;
|
||||||
|
height: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pos-btn:hover {
|
||||||
|
transform: scaleX(2) scaleY(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pos-btn--active .pos-btn__inner {
|
||||||
|
background-color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*=== .reference-btn ===*/
|
||||||
|
|
||||||
|
.reference-btn {
|
||||||
|
background: white;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: 5px dashed black;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 1rem;
|
||||||
|
width: 8rem;
|
||||||
|
height: 8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pos-container--global .reference-btn {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*=== .close-btn ===*/
|
||||||
|
|
||||||
|
.close-btn {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn::after {
|
||||||
|
content: '';
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*=== .overlay-content ===*/
|
||||||
|
|
||||||
|
.overlay-content {
|
||||||
|
display: flex;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: 1px solid black;
|
||||||
|
align-items: center;
|
||||||
|
justify-items: center;
|
||||||
|
background-color: #000;
|
||||||
|
padding: 1rem;
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host([active-pos^='center']) .pos-container--global .overlay-content {
|
||||||
|
width: 14rem;
|
||||||
|
height: 16rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host([active-pos^='bottom']) .pos-container--global .overlay-content,
|
||||||
|
:host([active-pos^='top']) .pos-container--global .overlay-content {
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host([active-pos^='left']) .pos-container--global .overlay-content,
|
||||||
|
:host([active-pos^='right']) .pos-container--global .overlay-content {
|
||||||
|
height: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host([active-pos^='center']) .pos-btn {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
refs = {
|
||||||
|
overlay: /** @type {{value: OverlayHost}} */ (createRef()),
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.placementMode = 'local';
|
||||||
|
/** @type {ViewportPlacement[]|LocalPlacement[]} */
|
||||||
|
this._placements = [];
|
||||||
|
this.simulateViewport = false;
|
||||||
|
this._activePos = 'top';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {{pos:ViewportPlacement|LocalPlacement}} opts
|
||||||
|
*/
|
||||||
|
async _updatePos({ pos }) {
|
||||||
|
this._activePos = pos;
|
||||||
|
|
||||||
|
const overlayEl = this.refs.overlay.value;
|
||||||
|
|
||||||
|
// @ts-ignore allow protected
|
||||||
|
if (overlayEl?._overlayCtrl) {
|
||||||
|
overlayEl.config = /** @type {Partial<OverlayConfig>} */ ({
|
||||||
|
popperConfig: { placement: pos },
|
||||||
|
viewportConfig: { placement: pos },
|
||||||
|
});
|
||||||
|
// TODO: these hacks below should not be needed. Fix when moving to floating-ui
|
||||||
|
// => animate different positions
|
||||||
|
// @ts-ignore allow protected
|
||||||
|
await overlayEl._overlayCtrl.hide();
|
||||||
|
// @ts-ignore allow protected
|
||||||
|
overlayEl._overlayCtrl.show();
|
||||||
|
overlayEl.config = /** @type {Partial<OverlayConfig>} */ ({
|
||||||
|
popperConfig: { placement: pos },
|
||||||
|
viewportConfig: { placement: pos },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('lit').PropertyValues } changedProperties
|
||||||
|
*/
|
||||||
|
firstUpdated(changedProperties) {
|
||||||
|
super.firstUpdated(changedProperties);
|
||||||
|
|
||||||
|
this._updatePos({ pos: this.placementMode === 'local' ? 'top' : 'center' });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('lit').PropertyValues } changedProperties
|
||||||
|
*/
|
||||||
|
update(changedProperties) {
|
||||||
|
if (changedProperties.has('placementMode')) {
|
||||||
|
if (this.placementMode === 'local') {
|
||||||
|
this._placements = [
|
||||||
|
`top-start`,
|
||||||
|
`top`,
|
||||||
|
`top-end`,
|
||||||
|
`right-start`,
|
||||||
|
`right`,
|
||||||
|
`right-end`,
|
||||||
|
`bottom-start`,
|
||||||
|
`bottom`,
|
||||||
|
`bottom-end`,
|
||||||
|
`left-start`,
|
||||||
|
`left`,
|
||||||
|
`left-end`,
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
this._placements = [
|
||||||
|
`center`,
|
||||||
|
`top-left`,
|
||||||
|
`top`,
|
||||||
|
`top-right`,
|
||||||
|
`right`,
|
||||||
|
`bottom-right`,
|
||||||
|
`bottom`,
|
||||||
|
`bottom-left`,
|
||||||
|
`left`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
super.update(changedProperties);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('lit').PropertyValues } changedProperties
|
||||||
|
*/
|
||||||
|
updated(changedProperties) {
|
||||||
|
super.updated(changedProperties);
|
||||||
|
|
||||||
|
if (changedProperties.has('placementMode')) {
|
||||||
|
this._activeConfig = {
|
||||||
|
placementMode: this.placementMode,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {{pos:string}} opts
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
_isActivePosBtn({ pos }) {
|
||||||
|
return pos === this._activePos;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return html`
|
||||||
|
<div class="pos-container pos-container--${this.placementMode}">
|
||||||
|
${this._placements.map(
|
||||||
|
pos =>
|
||||||
|
html`
|
||||||
|
<div class="pos-btn-wrapper pos-btn-wrapper--${pos}">
|
||||||
|
<button
|
||||||
|
@click="${() => this._updatePos({ pos })}"
|
||||||
|
class="pos-btn ${this._isActivePosBtn({ pos }) ? 'pos-btn--active' : ''}"
|
||||||
|
aria-label="${pos}"
|
||||||
|
>
|
||||||
|
<div class="pos-btn__inner"></div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
)}
|
||||||
|
|
||||||
|
<demo-overlay-el
|
||||||
|
opened
|
||||||
|
?simulate-viewport="${this.simulateViewport}"
|
||||||
|
${ref(
|
||||||
|
// @ts-ignore
|
||||||
|
this.refs.overlay,
|
||||||
|
)}
|
||||||
|
.config="${this._activeConfig}"
|
||||||
|
>
|
||||||
|
<button class="reference-btn" slot="invoker"></button>
|
||||||
|
<div class="overlay-content" slot="content"></div>
|
||||||
|
</demo-overlay-el>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
customElements.define('demo-overlay-positioning', DemoOverlayPositioning);
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
import { html, LitElement } from 'lit';
|
|
||||||
import { OverlayMixin } from '@lion/ui/overlays.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef {import('../types/OverlayConfig').OverlayConfig} OverlayConfig
|
|
||||||
*/
|
|
||||||
class DemoOverlaySystem extends OverlayMixin(LitElement) {
|
|
||||||
// eslint-disable-next-line class-methods-use-this
|
|
||||||
_defineOverlayConfig() {
|
|
||||||
return /** @type {OverlayConfig} */ ({
|
|
||||||
placementMode: 'global',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_setupOpenCloseListeners() {
|
|
||||||
super._setupOpenCloseListeners();
|
|
||||||
|
|
||||||
if (this._overlayInvokerNode) {
|
|
||||||
this._overlayInvokerNode.addEventListener('click', this.toggle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_teardownOpenCloseListeners() {
|
|
||||||
super._teardownOpenCloseListeners();
|
|
||||||
|
|
||||||
if (this._overlayInvokerNode) {
|
|
||||||
this._overlayInvokerNode.removeEventListener('click', this.toggle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return html`
|
|
||||||
<slot name="invoker"></slot>
|
|
||||||
<slot name="backdrop"></slot>
|
|
||||||
<div id="overlay-content-node-wrapper">
|
|
||||||
<slot name="content"></slot>
|
|
||||||
</div>
|
|
||||||
<div>popup is ${this.opened ? 'opened' : 'closed'}</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
customElements.define('demo-overlay-system', DemoOverlaySystem);
|
|
||||||
|
|
@ -0,0 +1,121 @@
|
||||||
|
/* eslint-disable max-classes-per-file */
|
||||||
|
/* eslint-disable import/no-extraneous-dependencies */
|
||||||
|
import { html, LitElement, css } from 'lit';
|
||||||
|
import { OverlayMixin } from '@lion/ui/overlays.js';
|
||||||
|
import { LionButton } from '@lion/ui/button.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {import('@lion/ui/types/overlays.js').OverlayConfig} OverlayConfig
|
||||||
|
*/
|
||||||
|
class DemoOverlaySystem extends OverlayMixin(LitElement) {
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
_defineOverlayConfig() {
|
||||||
|
return /** @type {OverlayConfig} */ ({
|
||||||
|
placementMode: 'global',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_setupOpenCloseListeners() {
|
||||||
|
super._setupOpenCloseListeners();
|
||||||
|
|
||||||
|
if (this._overlayInvokerNode) {
|
||||||
|
this._overlayInvokerNode.addEventListener('click', this.toggle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_teardownOpenCloseListeners() {
|
||||||
|
super._teardownOpenCloseListeners();
|
||||||
|
|
||||||
|
if (this._overlayInvokerNode) {
|
||||||
|
this._overlayInvokerNode.removeEventListener('click', this.toggle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return html`
|
||||||
|
<slot name="invoker"></slot>
|
||||||
|
<slot name="backdrop"></slot>
|
||||||
|
<slot name="content"></slot>
|
||||||
|
|
||||||
|
<div>popup is ${this.opened ? 'opened' : 'closed'}</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
customElements.define('demo-overlay-system', DemoOverlaySystem);
|
||||||
|
|
||||||
|
class DemoOverlay extends OverlayMixin(LitElement) {
|
||||||
|
static get styles() {
|
||||||
|
return [
|
||||||
|
css`
|
||||||
|
::slotted([slot='content']) {
|
||||||
|
background-color: #333;
|
||||||
|
color: white;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-button {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 16px;
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
_defineOverlayConfig() {
|
||||||
|
return /** @type {OverlayConfig} */ ({
|
||||||
|
placementMode: 'global',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_setupOpenCloseListeners() {
|
||||||
|
super._setupOpenCloseListeners();
|
||||||
|
|
||||||
|
if (this._overlayInvokerNode) {
|
||||||
|
this._overlayInvokerNode.addEventListener('click', this.toggle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_teardownOpenCloseListeners() {
|
||||||
|
super._teardownOpenCloseListeners();
|
||||||
|
|
||||||
|
if (this._overlayInvokerNode) {
|
||||||
|
this._overlayInvokerNode.removeEventListener('click', this.toggle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return html`
|
||||||
|
<slot name="invoker"></slot>
|
||||||
|
<slot name="backdrop"></slot>
|
||||||
|
<div id="overlay-content-node-wrapper">
|
||||||
|
<slot name="content"></slot>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
customElements.define('demo-overlay', DemoOverlay);
|
||||||
|
|
||||||
|
class DemoCloseButton extends LionButton {
|
||||||
|
static get styles() {
|
||||||
|
return [
|
||||||
|
css`
|
||||||
|
::host {
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
|
||||||
|
this.innerText = '⨯';
|
||||||
|
this.setAttribute('aria-label', 'Close');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
customElements.define('demo-close-button', DemoCloseButton);
|
||||||
|
|
@ -2,8 +2,8 @@
|
||||||
|
|
||||||
```js script
|
```js script
|
||||||
import { html } from '@mdjs/mdjs-preview';
|
import { html } from '@mdjs/mdjs-preview';
|
||||||
import './assets/demo-overlay-system.js';
|
import './assets/demo-overlay-system.mjs';
|
||||||
import './assets/applyDemoOverlayStyles.js';
|
import './assets/applyDemoOverlayStyles.mjs';
|
||||||
```
|
```
|
||||||
|
|
||||||
The `OverlayController` has many configuration options.
|
The `OverlayController` has many configuration options.
|
||||||
|
|
@ -351,11 +351,11 @@ Options:
|
||||||
export const viewportConfig = () => {
|
export const viewportConfig = () => {
|
||||||
const viewportConfig = {
|
const viewportConfig = {
|
||||||
placementMode: 'global',
|
placementMode: 'global',
|
||||||
viewportConfig: { placement: 'top-left' },
|
viewportConfig: { placement: 'bottom-left' },
|
||||||
};
|
};
|
||||||
return html`
|
return html`
|
||||||
<demo-overlay-system .config=${viewportConfig}>
|
<demo-overlay-system .config=${viewportConfig}>
|
||||||
<button slot="invoker">Click me to open the overlay in the top left corner!</button>
|
<button slot="invoker">Click me to open the overlay in the bottom left corner!</button>
|
||||||
<div slot="content" class="demo-overlay">
|
<div slot="content" class="demo-overlay">
|
||||||
Hello! You can close this notification here:
|
Hello! You can close this notification here:
|
||||||
<button
|
<button
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,8 @@ import '@lion/ui/define/lion-input-datepicker.js';
|
||||||
import '@lion/ui/define/lion-listbox.js';
|
import '@lion/ui/define/lion-listbox.js';
|
||||||
import '@lion/ui/define/lion-listbox.js';
|
import '@lion/ui/define/lion-listbox.js';
|
||||||
import '@lion/ui/define/lion-select-rich.js';
|
import '@lion/ui/define/lion-select-rich.js';
|
||||||
import './assets/demo-overlay-system.js';
|
import './assets/demo-overlay-system.mjs';
|
||||||
import './assets/applyDemoOverlayStyles.js';
|
import './assets/applyDemoOverlayStyles.mjs';
|
||||||
```
|
```
|
||||||
|
|
||||||
## Select Rich
|
## Select Rich
|
||||||
|
|
|
||||||
171
docs/fundamentals/systems/overlays/positioning.md
Normal file
171
docs/fundamentals/systems/overlays/positioning.md
Normal file
|
|
@ -0,0 +1,171 @@
|
||||||
|
# Systems >> Overlays >> Positioning ||10
|
||||||
|
|
||||||
|
```js script
|
||||||
|
import { html, render, LitElement } from '@mdjs/mdjs-preview';
|
||||||
|
import { ref, createRef } from 'lit/directives/ref.js';
|
||||||
|
import './assets/demo-overlay-positioning.mjs';
|
||||||
|
```
|
||||||
|
|
||||||
|
Overlays can have two different placement modes: relative to their anchor element and relative to the viewport.
|
||||||
|
Depending on screen size and viewing device, one placement mode might be suited better than the other.
|
||||||
|
|
||||||
|
> Note that, the placementMode option has the values 'local' (anchor) and 'global' (viewport). These refer to their
|
||||||
|
> legacy position in dom (global overlays were put in the body of the page). Since overlays are built with the native `<dialog>` element,
|
||||||
|
> no content is moved around anymore, so their names are a bit less intuitive.
|
||||||
|
|
||||||
|
## Relative to anchor
|
||||||
|
|
||||||
|
An anchor is usually the invoker button, it can also be a non interactive reference element.
|
||||||
|
Anchor placement uses Popper under the hood. It supports 9 positions:
|
||||||
|
`top-start`, `top`, `top-end`, `right-start`, `right`, `right-end`, `bottom-start`, `bottom`, `bottom-end`, `left-start`,`left`,`left-end`
|
||||||
|
|
||||||
|
```js story
|
||||||
|
export const localPositioning = () => html`<demo-overlay-positioning></demo-overlay-positioning>`;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Relative to viewport
|
||||||
|
|
||||||
|
Viewport placement uses the flexbox layout mode, leveraging the best browser capabilities when
|
||||||
|
the content or screen size updates.
|
||||||
|
Supported modes:
|
||||||
|
`center`, `top-left`, `top`, `top-right`, `right`, `bottom-right`, `bottom`, `bottom-left`, `left`
|
||||||
|
|
||||||
|
```js story
|
||||||
|
export const globalPositioning = () =>
|
||||||
|
html`<demo-overlay-positioning
|
||||||
|
placement-mode="global"
|
||||||
|
simulate-viewport
|
||||||
|
></demo-overlay-positioning>`;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notorious edge cases
|
||||||
|
|
||||||
|
These edge cases are not so much related to the edges of the viewport or the anchor, but more with the difficulties involved with the dom context of the overlay.
|
||||||
|
That is:
|
||||||
|
|
||||||
|
- a parent that has `overflow: hidden` applied
|
||||||
|
- a surrounding stacking context that does not allow to paint on top
|
||||||
|
- a transform applied to a parent
|
||||||
|
|
||||||
|
Note that our overlay uses the `<dialog>` element under the hood. It paints to the [top layer](https://github.com/whatwg/html/issues/4633) when it is configured as a modal overlay (that is, it uses `.showModal()` to open the dialog.)
|
||||||
|
|
||||||
|
### Overflow
|
||||||
|
|
||||||
|
A positioned parent with `overflow: hidden` can cause the overlay to be invisible. This can mainly occur in non-modal overlays being absolutely
|
||||||
|
positioned relative to their anchor.
|
||||||
|
|
||||||
|
See [css-tricks: popping-hidden-overflow](https://css-tricks.com/popping-hidden-overflow/#aa-the-solution)
|
||||||
|
|
||||||
|
#### Overflow: the problem
|
||||||
|
|
||||||
|
```js preview-story
|
||||||
|
export const edgeCaseOverflowProblem = () =>
|
||||||
|
html`
|
||||||
|
<div style="padding: 54px 24px 36px;">
|
||||||
|
<div
|
||||||
|
style="overflow: hidden; border: 1px black dashed; padding-top: 44px; padding-bottom: 16px;"
|
||||||
|
>
|
||||||
|
<div style="display: flex; justify-content: space-evenly; position: relative;">
|
||||||
|
<demo-overlay-el opened use-absolute>
|
||||||
|
<button slot="invoker" aria-label="local, non modal"></button>
|
||||||
|
<div slot="content">absolute (for demo)</div>
|
||||||
|
</demo-overlay-el>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Overflow: the solution
|
||||||
|
|
||||||
|
Two solutions are thinkable:
|
||||||
|
|
||||||
|
- use a modal overlay
|
||||||
|
- use the fixed positioning strategy for the non-modal overlay
|
||||||
|
|
||||||
|
Our overlay system makes sure that there's always a fixed layer that pops out of the hidden parent.
|
||||||
|
|
||||||
|
```js preview-story
|
||||||
|
export const edgeCaseOverflowSolution = () =>
|
||||||
|
html`
|
||||||
|
<div style="padding: 54px 24px 36px;">
|
||||||
|
<div
|
||||||
|
style="overflow: hidden; border: 1px black dashed; padding-top: 36px; padding-bottom: 16px;"
|
||||||
|
>
|
||||||
|
<div style="display: flex; justify-content: space-evenly; position: relative;">
|
||||||
|
<demo-overlay-el
|
||||||
|
opened
|
||||||
|
.config="${{ placementMode: 'local', trapsKeyboardFocus: false }}"
|
||||||
|
>
|
||||||
|
<button slot="invoker" aria-label="local, non modal"></button>
|
||||||
|
<div slot="content">no matter</div>
|
||||||
|
</demo-overlay-el>
|
||||||
|
|
||||||
|
<demo-overlay-el opened .config="${{ placementMode: 'local', trapsKeyboardFocus: true }}">
|
||||||
|
<button slot="invoker" aria-label="local, modal"></button>
|
||||||
|
<div slot="content">what configuration</div>
|
||||||
|
</demo-overlay-el>
|
||||||
|
|
||||||
|
<demo-overlay-el
|
||||||
|
opened
|
||||||
|
.config="${{ placementMode: 'local', popperConfig: { strategy: 'absolute' } }}"
|
||||||
|
>
|
||||||
|
<button slot="invoker" aria-label="local, absolute"></button>
|
||||||
|
<div slot="content">...it</div>
|
||||||
|
</demo-overlay-el>
|
||||||
|
|
||||||
|
<demo-overlay-el
|
||||||
|
opened
|
||||||
|
.config="${{ placementMode: 'local', popperConfig: { strategy: 'fixed' } }}"
|
||||||
|
>
|
||||||
|
<button slot="invoker" aria-label="local, fixed"></button>
|
||||||
|
<div slot="content">just</div>
|
||||||
|
</demo-overlay-el>
|
||||||
|
|
||||||
|
<demo-overlay-el opened .config="${{ placementMode: 'global' }}">
|
||||||
|
<button slot="invoker" aria-label="global"></button>
|
||||||
|
<div slot="content">works</div>
|
||||||
|
</demo-overlay-el>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Stacking context
|
||||||
|
|
||||||
|
When using non modal overlays, always make sure that the surrounding [stacking context](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/The_stacking_context) does not paint on top of your overlay.
|
||||||
|
The example below shows the difference between a modal and non-modal overlay placed in a stacking context with a lower priority than its parent/sibling contexts.
|
||||||
|
|
||||||
|
#### Stacking context: the problem
|
||||||
|
|
||||||
|
```js preview-story
|
||||||
|
export const edgeCaseStackProblem = () =>
|
||||||
|
html`
|
||||||
|
<div style="width: 300px; height: 300px; position: relative;">
|
||||||
|
<div
|
||||||
|
id="stacking-context-a"
|
||||||
|
style="position: absolute; z-index: 2; top: 0; width: 100px; height: 200px;"
|
||||||
|
>
|
||||||
|
I am on top and I don't care about your 9999
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
id="stacking-context-b"
|
||||||
|
style="position: absolute; z-index: 1; top: 0; width: 200px; height: 200px;"
|
||||||
|
>
|
||||||
|
<demo-overlay-el no-dialog-el style="overflow:hidden; position: relative;">
|
||||||
|
<button slot="invoker">invoke</button>
|
||||||
|
<div slot="content">
|
||||||
|
The overlay can never be in front, since the parent stacking context has a lower
|
||||||
|
priority than its sibling.
|
||||||
|
<div id="stacking-context-b-inner" style="position: absolute; z-index: 9999;">
|
||||||
|
So, even if we add a new stacking context in our overlay with z-index 9999, it will
|
||||||
|
never be painted on top.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</demo-overlay-el>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
```
|
||||||
|
|
@ -1,176 +0,0 @@
|
||||||
# Systems >> Overlays >> Scope ||50
|
|
||||||
|
|
||||||
The goal of this document is to specify the goal and duties of the overlay system, mainly by
|
|
||||||
identifying all different appearances and types of overlays.
|
|
||||||
|
|
||||||
## What is an overlay manager?
|
|
||||||
|
|
||||||
An overlay is a visual element that is painted on top of a page, breaking out of the regular
|
|
||||||
document flow. An overlay manager is a global repository keeping track of all different types of overlays. The need for a global housekeeping mainly arises when multiple overlays are opened simultaneously. As opposed to a single overlay, the overlay manager stores knowledge about:
|
|
||||||
|
|
||||||
- Whether the scroll behaviour of the body element can be manipulated.
|
|
||||||
- What space is available in the window for drawing new overlays.
|
|
||||||
|
|
||||||
The manager is in charge of rendering an overlay to the DOM. Therefore, a developer should be able to control:
|
|
||||||
|
|
||||||
- Its ‘physical position’ (where the dialog is attached). This can either be:
|
|
||||||
- globally: at root level of the DOM. This guarantees a total control over its painting, since the stacking context can be controlled from here and interfering parents (that set overflow values or transforms) can’t be apparent. Additionally, making a modal dialog requiring all surroundings to have aria-hidden="true", will be easier when the overlay is attached on body level.
|
|
||||||
- locally: next to the invoking element. This gives advantages for accessibility and(performant) rendering of an overlay next to its invoker on scroll and window resizes
|
|
||||||
- Toggling of the ‘shown’ state of the overlay
|
|
||||||
- Positioning preferences(for instance ‘bottom-left’) and strategies (ordered fallback preferences)
|
|
||||||
|
|
||||||
Presentation/styling of the overlay is out of the scope of the manager, except for its positioning in its context.
|
|
||||||
|
|
||||||
Accessibility is usually dependent on the type of overlay, its specific implementation and its browser/screen reader support (aria 1.0 vs 1.1). We strive for an optimum here by supporting 1.0 as a bare minimum and add 1.1 enrichments on top.
|
|
||||||
|
|
||||||
For every overlay, the manager has access to the overlay element and the invoker (and possible other elements that are needed for focus delegation as described in <https://www.w3.org/TR/wai-aria-practices/#dialog_modal> (notes).
|
|
||||||
|
|
||||||
## Defining different types of overlays
|
|
||||||
|
|
||||||
When browsing through the average ui library, one can encounter multiple names for occurrences of overlays. Here is a list of names encountered throughout the years:
|
|
||||||
|
|
||||||
- dialog
|
|
||||||
- modal
|
|
||||||
- popover
|
|
||||||
- popup
|
|
||||||
- popdown
|
|
||||||
- popper
|
|
||||||
- bubble
|
|
||||||
- balloon
|
|
||||||
- dropdown
|
|
||||||
- dropup
|
|
||||||
- tooltip
|
|
||||||
- layover
|
|
||||||
- overlay
|
|
||||||
- toast
|
|
||||||
- snackbar
|
|
||||||
- sheet (bottom, top, left, right)
|
|
||||||
- etc..
|
|
||||||
|
|
||||||
The problem with most of those terms is their lack of clear definition: what might be considered a tooltip in UI framework A, can be considered a popover in framework B. What can be called a modal in framework C, might actually be just a dialog. Etc etc…
|
|
||||||
|
|
||||||
### Official specifications
|
|
||||||
|
|
||||||
In order to avoid confusion and be as specification compliant as possible, it’s always a good idea to consult the W3C. This website shows a full list with specifications of accessible web widgets: <https://www.w3.org/TR/wai-aria-practices/>.
|
|
||||||
A great overview of all widget-, structure- and role relations can be found in the ontology diagram below:
|
|
||||||
|
|
||||||
<https://www.w3.org/WAI/PF/aria-1.1/rdf_model.svg>
|
|
||||||
|
|
||||||
Out of all the overlay names mentioned above, we can only identify the dialog and the tooltip as official roles. Let’s take a closer look at their definitions...
|
|
||||||
|
|
||||||
### Dialog
|
|
||||||
|
|
||||||
The dialog is described as follows by the W3C:
|
|
||||||
|
|
||||||
> “A dialog is a window overlaid on either the primary window or another dialog window. Windows
|
|
||||||
> under a modal dialog are inert. That is, users cannot interact with content outside an active
|
|
||||||
> dialog window. Inert content outside an active dialog is typically visually obscured or dimmed so
|
|
||||||
> it is difficult to discern, and in some implementations, attempts to interact with the inert
|
|
||||||
> content cause the dialog to close.
|
|
||||||
> Like non-modal dialogs, modal dialogs contain their tab sequence. That is, Tab and Shift + Tab do
|
|
||||||
> not move focus outside the dialog. However, unlike most non-modal dialogs, modal dialogs do not
|
|
||||||
> provide means for moving keyboard focus outside the dialog window without closing the dialog.”
|
|
||||||
|
|
||||||
- specification: <https://www.w3.org/TR/wai-aria-1.1/#dialog>
|
|
||||||
- widget description: <https://www.w3.org/TR/wai-aria-practices/#dialog_modal>
|
|
||||||
|
|
||||||
### Tooltip
|
|
||||||
|
|
||||||
According to W3C, a tooltip is described by the following:
|
|
||||||
|
|
||||||
> “A tooltip is a popup that displays information related to an element when the element receives.
|
|
||||||
> keyboard focus or the mouse hovers over it. It typically appears after a small delay and disappears.
|
|
||||||
> when Escape is pressed or on mouse out.
|
|
||||||
> Tooltip widgets do not receive focus. A hover that contains focusable elements can be made using.
|
|
||||||
> a non-modal dialog.”
|
|
||||||
|
|
||||||
- specification: <https://www.w3.org/TR/wai-aria-1.1/#tooltip>
|
|
||||||
- widget description: <https://www.w3.org/TR/wai-aria-practices/#tooltip>
|
|
||||||
|
|
||||||
What needs to be mentioned is that the W3C taskforce didn’t reach consensus yet about the above tooltip description. A good alternative resource:
|
|
||||||
<https://inclusive-components.design/tooltips-toggletips/>
|
|
||||||
|
|
||||||
### Dialog vs tooltip
|
|
||||||
|
|
||||||
Summarizing, the main differences between dialogs and tooltips are:
|
|
||||||
|
|
||||||
- Dialogs have a modal option, tooltips don’t
|
|
||||||
- Dialogs have interactive content, tooltips don’t
|
|
||||||
- Dialogs are opened via regular buttons (click/space/enter), tooltips act on focus/mouseover
|
|
||||||
|
|
||||||
### Other roles and concepts
|
|
||||||
|
|
||||||
Other roles worth mentioning are _alertdialog_ (a specific instance of the dialog for system alerts), select (an abstract role), _combobox_ and _menu_.
|
|
||||||
|
|
||||||
Also, the W3C document often refers to _popup_. This term is mentioned in the context of _combobox_, _listbox_, _grid_, _tree_, _dialog_ and _tooltip_. Therefore, one could say it could be a term
|
|
||||||
|
|
||||||
_aria-haspopup_ attribute needs to be mentioned: it can have values ‘menu’, ‘listbox’, ‘grid’, ’tree’ and ‘dialog’.
|
|
||||||
|
|
||||||
## Common Overlay Components
|
|
||||||
|
|
||||||
In our component library, we want to have the following overlay ‘child’ components:
|
|
||||||
|
|
||||||
- Dialog
|
|
||||||
- Tooltip
|
|
||||||
- Popover
|
|
||||||
- Dropdown
|
|
||||||
- Toast
|
|
||||||
- Sheet (bottom, top, left, right)
|
|
||||||
- Select
|
|
||||||
- Combobox/autocomplete
|
|
||||||
- Application menu
|
|
||||||
|
|
||||||
### Dialog Component
|
|
||||||
|
|
||||||
The dialog is pretty much the dialog as described in the W3C spec, having the modal option applied by default. The flexibility in focus delegation (see <https://www.w3.org/TR/wai-aria-practices/#dialog_modal> notes) is not implemented, however.
|
|
||||||
Addressing these:
|
|
||||||
|
|
||||||
- The first focusable element in the content: although delegate this focus management to the
|
|
||||||
implementing developer is highly preferred, since this is highly dependent on the moment the dialog content has finished rendering. This is something the overlay manager or dialog widget should not be aware of in order to provide.a clean and reliable component.
|
|
||||||
- The focusable element after a close: by default, this is the invoker. For different
|
|
||||||
behaviour, a reference should be supplied to a more logical element in the particular workflow.
|
|
||||||
|
|
||||||
### Tooltip Component
|
|
||||||
|
|
||||||
The tooltip is always invoked on hover and has no interactive content. See <https://inclusive-components.design/tooltips-toggletips/> (the tooltip example, not the toggle tip).
|
|
||||||
|
|
||||||
### Popover Component
|
|
||||||
|
|
||||||
The popover looks like a crossover between the dialog and the tooltip. Popovers are invoked on click. For non interactive content, the <https://inclusive-components.design/tooltips-toggletips/> toggletip could be applied. Whenever there would be a close button present, the ‘non interactiveness’ wouldn’t apply.
|
|
||||||
|
|
||||||
An alternative implementation: <https://whatsock.com/tsg/Coding%20Arena/Popups/Popup%20(Internal%20Content)/demo.htm>
|
|
||||||
This looks more like a small dialog, except that the page flow is respected (no rotating tab).
|
|
||||||
|
|
||||||
### Dropdown Component
|
|
||||||
|
|
||||||
The dropdown is not an official aria-widget and thus can’t be tied to a specific role. It exists in a lot of UI libraries and most of them share these properties:
|
|
||||||
|
|
||||||
- Preferred position is ‘down’.
|
|
||||||
- When no space at bottom, they show ‘up’ (in which. Case it behaves a as a dropup).
|
|
||||||
- Unlike popovers and tooltips, it will never be positioned horizontally.
|
|
||||||
|
|
||||||
Aliases are ‘popdown’, ‘pulldown’ and many others.
|
|
||||||
|
|
||||||
### Select Component
|
|
||||||
|
|
||||||
Implemented as a dropdown listbox with invoker button. Depending on the content of the options, the child list can either be of type listbox or grid.
|
|
||||||
See: <https://www.w3.org/TR/wai-aria-practices/examples/listbox/listbox-collapsible.html>
|
|
||||||
|
|
||||||
### Combobox Component
|
|
||||||
|
|
||||||
Implemented as a dropdown combobox with invoker input. Input is used as search filter and can contain autocomplete or autosuggest functionality:
|
|
||||||
See: <https://www.w3.org/TR/wai-aria-practices/#combobox>
|
|
||||||
|
|
||||||
### (Application) menu Component
|
|
||||||
|
|
||||||
Or sometimes called context-menu. Uses a dropdown to position its content.
|
|
||||||
See: <https://www.w3.org/WAI/tutorials/menus/flyout/>
|
|
||||||
Be aware not to use role=“menu”: <https://www.w3.org/WAI/tutorials/menus/application-menus/>
|
|
||||||
|
|
||||||
### Toast Component
|
|
||||||
|
|
||||||
See: <https://www.webcomponents.org/element/@polymer/paper-toast>. Should probably be implemented as an alertdialog.
|
|
||||||
|
|
||||||
### Sheet Component
|
|
||||||
|
|
||||||
See: <https://material.io/design/components/sheets-bottom.html>. Should probably be a global(modal) dialog.
|
|
||||||
|
|
@ -11,9 +11,9 @@ import {
|
||||||
withModalDialogConfig,
|
withModalDialogConfig,
|
||||||
} from '@lion/ui/overlays.js';
|
} from '@lion/ui/overlays.js';
|
||||||
|
|
||||||
import './assets/demo-overlay-system.js';
|
import './assets/demo-overlay-system.mjs';
|
||||||
import './assets/demo-overlay-backdrop.js';
|
import './assets/demo-overlay-backdrop.mjs';
|
||||||
import './assets/applyDemoOverlayStyles.js';
|
import './assets/applyDemoOverlayStyles.mjs';
|
||||||
import { ref, createRef } from 'lit/directives/ref.js';
|
import { ref, createRef } from 'lit/directives/ref.js';
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -329,7 +329,27 @@ _defineOverlay({ invokerNode, contentNode }) {
|
||||||
Below is another demo where you can toggle between configurations using buttons.
|
Below is another demo where you can toggle between configurations using buttons.
|
||||||
|
|
||||||
```js preview-story
|
```js preview-story
|
||||||
export const responsiveSwitching2 = () => html`
|
export const responsiveSwitching2 = () => {
|
||||||
|
const overlayRef = createRef();
|
||||||
|
const selectRef = createRef();
|
||||||
|
|
||||||
|
const getConfig = selectValue => {
|
||||||
|
switch (selectValue) {
|
||||||
|
case 'modaldialog':
|
||||||
|
return { ...withModalDialogConfig(), hasBackdrop: true };
|
||||||
|
case 'bottomsheet':
|
||||||
|
return { ...withBottomSheetConfig() };
|
||||||
|
case 'dropdown':
|
||||||
|
return { ...withDropdownConfig(), hasBackdrop: false };
|
||||||
|
default:
|
||||||
|
return { ...withModalDialogConfig(), hasBackdrop: true };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const onSelectChange = e => {
|
||||||
|
overlayRef.value.config = getConfig(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
return html`
|
||||||
<style>
|
<style>
|
||||||
.demo-overlay {
|
.demo-overlay {
|
||||||
background-color: white;
|
background-color: white;
|
||||||
|
|
@ -337,44 +357,27 @@ export const responsiveSwitching2 = () => html`
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
Change config to:
|
Change config to:
|
||||||
<button
|
|
||||||
@click=${e => {
|
<select ${ref(selectRef)} @change="${onSelectChange}">
|
||||||
e.target.parentElement.querySelector('#respSwitchOverlay').config = {
|
<option value="modaldialog">Modal Dialog</option>
|
||||||
...withModalDialogConfig(),
|
<option value="bottomsheet">Bottom Sheet</option>
|
||||||
};
|
<option value="dropdown">Dropdown</option>
|
||||||
}}
|
</select>
|
||||||
>
|
|
||||||
Modal Dialog
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
@click=${e => {
|
|
||||||
e.target.parentElement.querySelector('#respSwitchOverlay').config = {
|
|
||||||
...withBottomSheetConfig(),
|
|
||||||
};
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Bottom Sheet
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
@click=${e => {
|
|
||||||
e.target.parentElement.querySelector('#respSwitchOverlay').config = {
|
|
||||||
...withDropdownConfig(),
|
|
||||||
};
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Dropdown
|
|
||||||
</button>
|
|
||||||
<br />
|
<br />
|
||||||
<demo-overlay-system id="respSwitchOverlay" .config=${{ ...withBottomSheetConfig() }}>
|
<demo-overlay-system ${ref(overlayRef)} .config=${getConfig(selectRef.value?.value)}>
|
||||||
<button slot="invoker">Click me to open the overlay!</button>
|
<button slot="invoker">Click me to open the overlay!</button>
|
||||||
<div slot="content" class="demo-overlay">
|
<div slot="content" class="demo-overlay">
|
||||||
Hello! You can close this notification here:
|
Hello! You can close this notification here:
|
||||||
<button @click=${e => e.target.dispatchEvent(new Event('close-overlay', { bubbles: true }))}>
|
<button
|
||||||
|
@click=${e => e.target.dispatchEvent(new Event('close-overlay', { bubbles: true }))}
|
||||||
|
>
|
||||||
⨯
|
⨯
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</demo-overlay-system>
|
</demo-overlay-system>
|
||||||
`;
|
`;
|
||||||
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
## Opened state
|
## Opened state
|
||||||
|
|
|
||||||
|
|
@ -57,15 +57,11 @@ describe('lion-dialog', () => {
|
||||||
</lion-dialog>
|
</lion-dialog>
|
||||||
`);
|
`);
|
||||||
|
|
||||||
// @ts-expect-error you're not allowed to call protected _overlayInvokerNode in public context, but for testing it's okay
|
// @ts-expect-error [allow-protected] in tests
|
||||||
el._overlayInvokerNode.click();
|
el._overlayInvokerNode.click();
|
||||||
expect(el.opened).to.be.true;
|
expect(el.opened).to.be.true;
|
||||||
|
|
||||||
const overlaysContainer = /** @type {HTMLElement} */ (
|
const nestedDialog = /** @type {LionDialog} */ (el.querySelector('lion-dialog'));
|
||||||
document.querySelector('.global-overlays')
|
|
||||||
);
|
|
||||||
const wrapperNode = Array.from(overlaysContainer.children)[1];
|
|
||||||
const nestedDialog = /** @type {LionDialog} */ (wrapperNode.querySelector('lion-dialog'));
|
|
||||||
// @ts-expect-error you're not allowed to call protected _overlayInvokerNode in public context, but for testing it's okay
|
// @ts-expect-error you're not allowed to call protected _overlayInvokerNode in public context, but for testing it's okay
|
||||||
nestedDialog?._overlayInvokerNode.click();
|
nestedDialog?._overlayInvokerNode.click();
|
||||||
expect(nestedDialog.opened).to.be.true;
|
expect(nestedDialog.opened).to.be.true;
|
||||||
|
|
|
||||||
|
|
@ -394,7 +394,7 @@ export function runListboxMixinSuite(customConfig = {}) {
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
await el.updateComplete; // need 2 awaits as overlay.show is an async function
|
await el.updateComplete; // need 2 awaits as overlay.show is an async function
|
||||||
|
|
||||||
await expect(el).to.be.accessible();
|
await expect(el).to.be.accessible({ ignoredRules: ['aria-allowed-role'] });
|
||||||
});
|
});
|
||||||
|
|
||||||
// NB: regular listbox is always 'opened', but needed for combobox and select-rich
|
// NB: regular listbox is always 'opened', but needed for combobox and select-rich
|
||||||
|
|
@ -405,7 +405,7 @@ export function runListboxMixinSuite(customConfig = {}) {
|
||||||
<${optionTag} .choiceValue=${20}>Item 2</${optionTag}>
|
<${optionTag} .choiceValue=${20}>Item 2</${optionTag}>
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`);
|
`);
|
||||||
await expect(el).to.be.accessible();
|
await expect(el).to.be.accessible({ ignoredRules: ['aria-allowed-role'] });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not have a tabindex', async () => {
|
it('does not have a tabindex', async () => {
|
||||||
|
|
@ -430,7 +430,7 @@ export function runListboxMixinSuite(customConfig = {}) {
|
||||||
const el = await fixture(html`
|
const el = await fixture(html`
|
||||||
<${tag} opened has-no-default-selected autocomplete="none" show-all-on-empty>
|
<${tag} opened has-no-default-selected autocomplete="none" show-all-on-empty>
|
||||||
<${optionTag} .choiceValue=${'10'} id="first">Item 1</${optionTag}>
|
<${optionTag} .choiceValue=${'10'} id="first">Item 1</${optionTag}>
|
||||||
<${optionTag} .choiceValue=${'20'} checked id="second">Item 2</${optionTag}>
|
<${optionTag} .choiceValue=${'20'} id="second">Item 2</${optionTag}>
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`);
|
`);
|
||||||
const { _activeDescendantOwnerNode } = getListboxMembers(el);
|
const { _activeDescendantOwnerNode } = getListboxMembers(el);
|
||||||
|
|
|
||||||
|
|
@ -1,34 +1,114 @@
|
||||||
import { EventTargetShim } from '@lion/ui/core.js';
|
import { EventTargetShim } from '@lion/ui/core.js';
|
||||||
|
import { adoptStyles } from 'lit';
|
||||||
import { overlays } from './singleton.js';
|
import { overlays } from './singleton.js';
|
||||||
import { containFocus } from './utils/contain-focus.js';
|
import { containFocus } from './utils/contain-focus.js';
|
||||||
|
import { globalOverlaysStyle } from './globalOverlaysStyle.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {import('../types/OverlayConfig.js').OverlayConfig} OverlayConfig
|
* @typedef {import('@lion/ui/types/overlays.js').OverlayConfig} OverlayConfig
|
||||||
* @typedef {import('../types/OverlayConfig.js').ViewportConfig} ViewportConfig
|
* @typedef {import('@lion/ui/types/overlays.js').ViewportConfig} ViewportConfig
|
||||||
* @typedef {import('@popperjs/core/lib/popper.js').createPopper} Popper
|
* @typedef {import('@popperjs/core').createPopper} Popper
|
||||||
* @typedef {import('@popperjs/core/lib/popper.js').Options} PopperOptions
|
* @typedef {import('@popperjs/core').Options} PopperOptions
|
||||||
* @typedef {import('@popperjs/core/lib/enums.js').Placement} Placement
|
* @typedef {import('@popperjs/core').Placement} Placement
|
||||||
* @typedef {{ createPopper: Popper }} PopperModule
|
* @typedef {{ createPopper: Popper }} PopperModule
|
||||||
* @typedef {'setup'|'init'|'teardown'|'before-show'|'show'|'hide'|'add'|'remove'} OverlayPhase
|
* @typedef {'setup'|'init'|'teardown'|'before-show'|'show'|'hide'|'add'|'remove'} OverlayPhase
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* From:
|
||||||
|
* - wrappingDialogNodeL1: `<dialog role="none"/>`
|
||||||
|
* - contentWrapperNodeL2: `<div id="content-wrapper-node"/>`
|
||||||
|
* - contentNodeL3: `<div slot="my-content"/>`
|
||||||
|
* To:
|
||||||
|
* ```html
|
||||||
|
* <dialog role="none">
|
||||||
|
* <div id="content-wrapper-node">
|
||||||
|
* <!-- this was the (slot for) original content node -->
|
||||||
|
* <slot name="my-content"></slot>
|
||||||
|
* </div>
|
||||||
|
* </dialog>
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* `<slot name="my-content">` belonging to `<div slot="content"/>` will be wrapped with wrappingDialogNodeL1 and contentWrapperNodeL2
|
||||||
|
* inside shadow dom. With the help of temp markers, `<slot name="my-content">`'s original position will be respected.
|
||||||
|
*
|
||||||
|
* @param {{ wrappingDialogNodeL1:HTMLDialogElement|HTMLDivElement; contentWrapperNodeL2:Element; contentNodeL3: Element }} opts
|
||||||
|
*/
|
||||||
|
function rearrangeNodes({ wrappingDialogNodeL1, contentWrapperNodeL2, contentNodeL3 }) {
|
||||||
|
if (!(contentWrapperNodeL2.isConnected || contentNodeL3.isConnected)) {
|
||||||
|
throw new Error(
|
||||||
|
'[OverlayController] Could not find a render target, since the provided contentNode is not connected to the DOM. Make sure that it is connected, e.g. by doing "document.body.appendChild(contentNode)", before passing it on.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let parentElement;
|
||||||
|
const tempMarker = document.createComment('temp-marker');
|
||||||
|
|
||||||
|
if (contentWrapperNodeL2.isConnected) {
|
||||||
|
// This is the case when contentWrapperNode (living in shadow dom, wrapping <slot name="my-content-outlet">) is already provided via controller.
|
||||||
|
parentElement = contentWrapperNodeL2.parentElement || contentWrapperNodeL2.getRootNode();
|
||||||
|
parentElement.insertBefore(tempMarker, contentWrapperNodeL2);
|
||||||
|
// Wrap...
|
||||||
|
wrappingDialogNodeL1.appendChild(contentWrapperNodeL2);
|
||||||
|
}
|
||||||
|
// if contentNodeL3.isConnected
|
||||||
|
else {
|
||||||
|
const contentIsProjected = contentNodeL3.assignedSlot;
|
||||||
|
if (contentIsProjected) {
|
||||||
|
parentElement =
|
||||||
|
contentNodeL3.assignedSlot.parentElement || contentNodeL3.assignedSlot.getRootNode();
|
||||||
|
parentElement.insertBefore(tempMarker, contentNodeL3.assignedSlot);
|
||||||
|
wrappingDialogNodeL1.appendChild(contentWrapperNodeL2);
|
||||||
|
// Important: we do not move around contentNodeL3, but the assigned slot
|
||||||
|
contentWrapperNodeL2.appendChild(contentNodeL3.assignedSlot);
|
||||||
|
} else {
|
||||||
|
parentElement = contentNodeL3.parentElement || contentNodeL3.getRootNode();
|
||||||
|
parentElement.insertBefore(tempMarker, contentNodeL3);
|
||||||
|
wrappingDialogNodeL1.appendChild(contentWrapperNodeL2);
|
||||||
|
contentWrapperNodeL2.appendChild(contentNodeL3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* From:
|
||||||
|
* ```html
|
||||||
|
* #shadow-root:
|
||||||
|
* <div>
|
||||||
|
* <!-- tempMarker -->
|
||||||
|
* <slot name="x"/>
|
||||||
|
* </div>
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* To:
|
||||||
|
* ```html
|
||||||
|
* #shadow-root:
|
||||||
|
* <div>
|
||||||
|
* <!-- tempMarker -->
|
||||||
|
* <dialog role="none">
|
||||||
|
* <div id="content-wrapper-node">
|
||||||
|
* <slot name="x"/>
|
||||||
|
* </div>
|
||||||
|
* </dialog>
|
||||||
|
* </div>
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
parentElement.insertBefore(wrappingDialogNodeL1, tempMarker);
|
||||||
|
parentElement?.removeChild(tempMarker);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {Promise<PopperModule>}
|
* @returns {Promise<PopperModule>}
|
||||||
*/
|
*/
|
||||||
async function preloadPopper() {
|
async function preloadPopper() {
|
||||||
/* eslint-disable import/no-extraneous-dependencies */ // -- [external]: import complains about untyped module, but we typecast it ourselves
|
// @ts-ignore [external]: import complains about untyped module, but we typecast it ourselves
|
||||||
// @ts-ignore
|
|
||||||
return /** @type {* & Promise<PopperModule>} */ (import('@popperjs/core/dist/esm/popper.js'));
|
return /** @type {* & Promise<PopperModule>} */ (import('@popperjs/core/dist/esm/popper.js'));
|
||||||
/* eslint-enable import/no-extraneous-dependencies */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const GLOBAL_OVERLAYS_CONTAINER_CLASS = 'global-overlays__overlay-container';
|
|
||||||
const GLOBAL_OVERLAYS_CLASS = 'global-overlays__overlay';
|
|
||||||
// @ts-expect-error [external]: CSS not yet typed
|
// @ts-expect-error [external]: CSS not yet typed
|
||||||
const supportsCSSTypedObject = window.CSS?.number && document.body.attributeStyleMap?.set;
|
const supportsCSSTypedObject = window.CSS?.number && document.body.attributeStyleMap?.set;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @desc OverlayController is the fundament for every single type of overlay. With the right
|
* OverlayController is the fundament for every single type of overlay. With the right
|
||||||
* 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.
|
||||||
*
|
*
|
||||||
|
|
@ -74,7 +154,7 @@ const supportsCSSTypedObject = window.CSS?.number && document.body.attributeStyl
|
||||||
* #### html structure for a content projected node
|
* #### html structure for a content projected node
|
||||||
* <div id="contentWrapperNode">
|
* <div id="contentWrapperNode">
|
||||||
* <slot name="contentNode"></slot>
|
* <slot name="contentNode"></slot>
|
||||||
* <div x-arrow></div>
|
* <div data-popper-arrow></div>
|
||||||
* </div>
|
* </div>
|
||||||
*
|
*
|
||||||
* Structure above depicts [l4]
|
* Structure above depicts [l4]
|
||||||
|
|
@ -97,6 +177,9 @@ export class OverlayController extends EventTargetShim {
|
||||||
/** @private */
|
/** @private */
|
||||||
this.__sharedConfig = config;
|
this.__sharedConfig = config;
|
||||||
|
|
||||||
|
/** @type {OverlayConfig} */
|
||||||
|
this.config = {};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {OverlayConfig}
|
* @type {OverlayConfig}
|
||||||
* @protected
|
* @protected
|
||||||
|
|
@ -123,7 +206,7 @@ export class OverlayController extends EventTargetShim {
|
||||||
handlesAccessibility: false,
|
handlesAccessibility: false,
|
||||||
popperConfig: {
|
popperConfig: {
|
||||||
placement: 'top',
|
placement: 'top',
|
||||||
strategy: 'absolute',
|
strategy: 'fixed',
|
||||||
modifiers: [
|
modifiers: [
|
||||||
{
|
{
|
||||||
name: 'preventOverflow',
|
name: 'preventOverflow',
|
||||||
|
|
@ -156,6 +239,8 @@ export class OverlayController extends EventTargetShim {
|
||||||
viewportConfig: {
|
viewportConfig: {
|
||||||
placement: 'center',
|
placement: 'center',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
zIndex: 9999,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.manager.add(this);
|
this.manager.add(this);
|
||||||
|
|
@ -163,25 +248,11 @@ export class OverlayController extends EventTargetShim {
|
||||||
this._contentId = `overlay-content--${Math.random().toString(36).substr(2, 10)}`;
|
this._contentId = `overlay-content--${Math.random().toString(36).substr(2, 10)}`;
|
||||||
/** @private */
|
/** @private */
|
||||||
this.__originalAttrs = new Map();
|
this.__originalAttrs = new Map();
|
||||||
if (this._defaultConfig.contentNode) {
|
|
||||||
if (!this._defaultConfig.contentNode.isConnected) {
|
|
||||||
throw new Error(
|
|
||||||
'[OverlayController] Could not find a render target, since the provided contentNode is not connected to the DOM. Make sure that it is connected, e.g. by doing "document.body.appendChild(contentNode)", before passing it on.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
this.__isContentNodeProjected = Boolean(this._defaultConfig.contentNode.assignedSlot);
|
|
||||||
}
|
|
||||||
this.updateConfig(config);
|
this.updateConfig(config);
|
||||||
/** @private */
|
/** @private */
|
||||||
this.__hasActiveTrapsKeyboardFocus = false;
|
this.__hasActiveTrapsKeyboardFocus = false;
|
||||||
/** @private */
|
/** @private */
|
||||||
this.__hasActiveBackdrop = true;
|
this.__hasActiveBackdrop = true;
|
||||||
/**
|
|
||||||
* @type {HTMLElement | undefined}
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
this.__backdropNodeToBeTornDown = undefined;
|
|
||||||
|
|
||||||
/** @private */
|
/** @private */
|
||||||
this.__escKeyHandler = this.__escKeyHandler.bind(this);
|
this.__escKeyHandler = this.__escKeyHandler.bind(this);
|
||||||
}
|
}
|
||||||
|
|
@ -196,10 +267,10 @@ export class OverlayController extends EventTargetShim {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The contentWrapperNode
|
* The contentWrapperNode
|
||||||
* @type {HTMLElement}
|
* @type {HTMLDialogElement | HTMLDivElement}
|
||||||
*/
|
*/
|
||||||
get content() {
|
get content() {
|
||||||
return /** @type {HTMLElement} */ (this.contentWrapperNode);
|
return /** @type {HTMLDialogElement | HTMLDivElement} */ (this.__wrappingDialogNode);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -385,28 +456,6 @@ export class OverlayController extends EventTargetShim {
|
||||||
return /** @type {ViewportConfig} */ (this.config?.viewportConfig);
|
return /** @type {ViewportConfig} */ (this.config?.viewportConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 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).
|
|
||||||
* @type {HTMLElement}
|
|
||||||
* @protected
|
|
||||||
*/
|
|
||||||
get _renderTarget() {
|
|
||||||
/** config [g1] */
|
|
||||||
if (this.placementMode === 'global') {
|
|
||||||
return this.manager.globalRootNode;
|
|
||||||
}
|
|
||||||
/** config [l2] or [l4] */
|
|
||||||
if (this.__isContentNodeProjected) {
|
|
||||||
// @ts-expect-error [external]: fix Node types
|
|
||||||
return this.__originalContentParent?.getRootNode().host;
|
|
||||||
}
|
|
||||||
/** config [l1] or [l3] */
|
|
||||||
return /** @type {HTMLElement} */ (this.__originalContentParent);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @desc The element our local overlay will be positioned relative to.
|
* @desc The element our local overlay will be positioned relative to.
|
||||||
* @type {HTMLElement | undefined}
|
* @type {HTMLElement | undefined}
|
||||||
|
|
@ -420,12 +469,8 @@ export class OverlayController extends EventTargetShim {
|
||||||
* @param {number} value
|
* @param {number} value
|
||||||
*/
|
*/
|
||||||
set elevation(value) {
|
set elevation(value) {
|
||||||
if (this.contentWrapperNode) {
|
// @ts-expect-error find out why config would/could be undfined
|
||||||
this.contentWrapperNode.style.zIndex = `${value}`;
|
this.__wrappingDialogNode.style.zIndex = `${this.config.zIndex + value}`;
|
||||||
}
|
|
||||||
if (this.backdropNode) {
|
|
||||||
this.backdropNode.style.zIndex = `${value}`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -472,9 +517,9 @@ export class OverlayController extends EventTargetShim {
|
||||||
};
|
};
|
||||||
|
|
||||||
/** @private */
|
/** @private */
|
||||||
this.__validateConfiguration(/** @type {OverlayConfig} */ (this.config));
|
this.__validateConfiguration(this.config);
|
||||||
/** @protected */
|
/** @protected */
|
||||||
this._init({ cfgToAdd });
|
this._init();
|
||||||
/** @private */
|
/** @private */
|
||||||
this.__elementToFocusAfterHide = undefined;
|
this.__elementToFocusAfterHide = undefined;
|
||||||
}
|
}
|
||||||
|
|
@ -498,16 +543,6 @@ export class OverlayController extends EventTargetShim {
|
||||||
if (!newConfig.contentNode) {
|
if (!newConfig.contentNode) {
|
||||||
throw new Error('[OverlayController] You need to provide a .contentNode');
|
throw new Error('[OverlayController] You need to provide a .contentNode');
|
||||||
}
|
}
|
||||||
if (this.__isContentNodeProjected && !newConfig.contentWrapperNode) {
|
|
||||||
throw new Error(
|
|
||||||
'[OverlayController] You need to provide a .contentWrapperNode when .contentNode is projected',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (newConfig.isTooltip && newConfig.placementMode !== 'local') {
|
|
||||||
throw new Error(
|
|
||||||
'[OverlayController] .isTooltip should be configured with .placementMode "local"',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (newConfig.isTooltip && !newConfig.handlesAccessibility) {
|
if (newConfig.isTooltip && !newConfig.handlesAccessibility) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'[OverlayController] .isTooltip only takes effect when .handlesAccessibility is enabled',
|
'[OverlayController] .isTooltip only takes effect when .handlesAccessibility is enabled',
|
||||||
|
|
@ -519,85 +554,95 @@ export class OverlayController extends EventTargetShim {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {{ cfgToAdd: OverlayConfig }} options
|
|
||||||
* @protected
|
* @protected
|
||||||
*/
|
*/
|
||||||
_init({ cfgToAdd }) {
|
_init() {
|
||||||
this.__initContentWrapperNode({ cfgToAdd });
|
if (!this.__contentHasBeenInitialized) {
|
||||||
this.__initConnectionTarget();
|
this.__initContentDomStructure();
|
||||||
|
this.__contentHasBeenInitialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset all positioning styles (local, c.q. Popper) and classes (global)
|
||||||
|
this.contentWrapperNode.removeAttribute('style');
|
||||||
|
this.contentWrapperNode.removeAttribute('class');
|
||||||
|
|
||||||
if (this.placementMode === 'local') {
|
if (this.placementMode === 'local') {
|
||||||
// Lazily load Popper if not done yet
|
// Lazily load Popper if not done yet
|
||||||
if (!OverlayController.popperModule) {
|
if (!OverlayController.popperModule) {
|
||||||
// a@ts-expect-error FIXME: for some reason createPopper is missing here
|
|
||||||
OverlayController.popperModule = preloadPopper();
|
OverlayController.popperModule = preloadPopper();
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
const rootNode = /** @type {ShadowRoot} */ (this.contentWrapperNode.getRootNode());
|
||||||
|
adoptStyles(rootNode, [...(rootNode.adoptedStyleSheets || []), globalOverlaysStyle]);
|
||||||
}
|
}
|
||||||
this._handleFeatures({ phase: 'init' });
|
this._handleFeatures({ phase: 'init' });
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @private */
|
|
||||||
__initConnectionTarget() {
|
|
||||||
// Now, add our node to the right place in dom (renderTarget)
|
|
||||||
if (this.contentWrapperNode !== this.__prevConfig?.contentWrapperNode) {
|
|
||||||
if (this.config?.placementMode === 'global' || !this.__isContentNodeProjected) {
|
|
||||||
/** @type {HTMLElement} */
|
|
||||||
(this.contentWrapperNode).appendChild(this.contentNode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this._renderTarget) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.__isContentNodeProjected && this.placementMode === 'local') {
|
|
||||||
// We add the contentNode in its slot, so that it will be projected by contentWrapperNode
|
|
||||||
this._renderTarget.appendChild(this.contentNode);
|
|
||||||
} else {
|
|
||||||
const isInsideRenderTarget = this._renderTarget === this.contentWrapperNode.parentNode;
|
|
||||||
const nodeContainsTarget = this.contentWrapperNode.contains(this._renderTarget);
|
|
||||||
if (!isInsideRenderTarget && !nodeContainsTarget) {
|
|
||||||
// contentWrapperNode becomes the direct (non projected) parent of contentNode
|
|
||||||
this._renderTarget.appendChild(this.contentWrapperNode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cleanup ._contentWrapperNode. We do this, because creating a fresh wrapper
|
* Here we arrange our content node via:
|
||||||
* can lead to problems with event listeners...
|
* 1. HTMLDialogElement: the content will always be painted to the browser's top layer
|
||||||
* @param {{ cfgToAdd: OverlayConfig }} options
|
* - no matter what context the contentNode lives in, the overlay will be painted correctly via the <dialog> element,
|
||||||
|
* even if 'overflow:hidden' or a css transform is applied in its parent hierarchy.
|
||||||
|
* - the dialog element will be unstyled, but will span the whole screen
|
||||||
|
* - a backdrop element will be a child of the dialog element, so it leverages the capabilities of the parent
|
||||||
|
* (filling the whole screen if wanted an always painted to top layer)
|
||||||
|
* 2. ContentWrapper: the content receives the right positioning styles in a clean/non conflicting way:
|
||||||
|
* - local positioning: receive inline (position) styling that can never conflict with the already existing computed styles
|
||||||
|
* - global positioning: receive flex (child) classes that position the content correctly relative to the viewport
|
||||||
|
*
|
||||||
|
* The resulting structure that will be created looks like this:
|
||||||
|
*
|
||||||
|
* ...
|
||||||
|
* <dialog role="none">
|
||||||
|
* <div id="optional-backdrop"></div>
|
||||||
|
* <div id="content-wrapper-node">
|
||||||
|
* <!-- this was the (slot for) original content node -->
|
||||||
|
* <slot name="content"></slot>
|
||||||
|
* </div>
|
||||||
|
* </dialog>
|
||||||
|
* ...
|
||||||
|
*
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
__initContentWrapperNode({ cfgToAdd }) {
|
__initContentDomStructure() {
|
||||||
if (this.config?.contentWrapperNode && this.placementMode === 'local') {
|
const wrappingDialogElement = document.createElement(
|
||||||
/** config [l2],[l3],[l4] */
|
this.config?._noDialogEl ? 'div' : 'dialog',
|
||||||
this.__contentWrapperNode = this.config.contentWrapperNode;
|
);
|
||||||
} else {
|
// We use a dialog for its visual capabilities: it renders to the top layer.
|
||||||
/** config [l1],[g1] */
|
// A11y will depend on the type of overlay and is arranged on contentNode level.
|
||||||
|
// Also see: https://www.scottohara.me/blog/2019/03/05/open-dialog.html
|
||||||
|
wrappingDialogElement.setAttribute('role', 'none');
|
||||||
|
// N.B. position: fixed is needed to escape out of 'overflow: hidden'
|
||||||
|
// We give a high z-index for non-modal dialogs, so that we at least win from all siblings of our
|
||||||
|
// parent stacking context
|
||||||
|
wrappingDialogElement.style.cssText = `display:none; background-image: none; border-style: none; padding: 0; z-index: ${this.config.zIndex};`;
|
||||||
|
this.__wrappingDialogNode = wrappingDialogElement;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Based on the configuration of the developer, multiple scenarios are accounted for
|
||||||
|
* A. We already have a contentWrapperNode ()
|
||||||
|
*/
|
||||||
|
if (!this.config?.contentWrapperNode) {
|
||||||
this.__contentWrapperNode = document.createElement('div');
|
this.__contentWrapperNode = document.createElement('div');
|
||||||
}
|
}
|
||||||
|
this.contentWrapperNode.setAttribute('data-id', 'content-wrapper');
|
||||||
|
|
||||||
this.contentWrapperNode.style.cssText = '';
|
rearrangeNodes({
|
||||||
this.contentWrapperNode.style.display = 'none';
|
wrappingDialogNodeL1: wrappingDialogElement,
|
||||||
|
contentWrapperNodeL2: this.contentWrapperNode,
|
||||||
|
contentNodeL3: this.contentNode,
|
||||||
|
});
|
||||||
|
// @ts-ignore
|
||||||
|
wrappingDialogElement.open = true;
|
||||||
|
|
||||||
|
this.__wrappingDialogNode.style.display = 'none';
|
||||||
|
this.contentWrapperNode.style.zIndex = '1';
|
||||||
|
|
||||||
if (getComputedStyle(this.contentNode).position === 'absolute') {
|
if (getComputedStyle(this.contentNode).position === 'absolute') {
|
||||||
// Having a _contWrapperNode and a contentNode with 'position:absolute' results in
|
// Having a _contWrapperNode and a contentNode with 'position:absolute' results in
|
||||||
// computed height of 0...
|
// computed height of 0...
|
||||||
this.contentNode.style.position = 'static';
|
this.contentNode.style.position = 'static';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.__isContentNodeProjected && this.contentWrapperNode.isConnected) {
|
|
||||||
// We need to keep track of the original local context.
|
|
||||||
/** config [l2], [l4] */
|
|
||||||
this.__originalContentParent = /** @type {HTMLElement} */ (
|
|
||||||
this.contentWrapperNode.parentNode
|
|
||||||
);
|
|
||||||
} else if (cfgToAdd.contentNode && cfgToAdd.contentNode.isConnected) {
|
|
||||||
// We need to keep track of the original local context.
|
|
||||||
/** config [l1], [l3], [g1] */
|
|
||||||
this.__originalContentParent = /** @type {HTMLElement} */ (this.contentNode?.parentNode);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -613,7 +658,7 @@ export class OverlayController extends EventTargetShim {
|
||||||
if (phase === 'setup') {
|
if (phase === 'setup') {
|
||||||
const zIndexNumber = Number(getComputedStyle(this.contentNode).zIndex);
|
const zIndexNumber = Number(getComputedStyle(this.contentNode).zIndex);
|
||||||
if (zIndexNumber < 1 || Number.isNaN(zIndexNumber)) {
|
if (zIndexNumber < 1 || Number.isNaN(zIndexNumber)) {
|
||||||
this.contentWrapperNode.style.zIndex = '1';
|
this.contentNode.style.zIndex = '1';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -686,7 +731,7 @@ export class OverlayController extends EventTargetShim {
|
||||||
}
|
}
|
||||||
|
|
||||||
get isShown() {
|
get isShown() {
|
||||||
return Boolean(this.contentWrapperNode.style.display !== 'none');
|
return Boolean(this.__wrappingDialogNode?.style.display !== 'none');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -717,7 +762,11 @@ export class OverlayController extends EventTargetShim {
|
||||||
const event = new CustomEvent('before-show', { cancelable: true });
|
const event = new CustomEvent('before-show', { cancelable: true });
|
||||||
this.dispatchEvent(event);
|
this.dispatchEvent(event);
|
||||||
if (!event.defaultPrevented) {
|
if (!event.defaultPrevented) {
|
||||||
this.contentWrapperNode.style.display = '';
|
if (this.__wrappingDialogNode instanceof HTMLDialogElement) {
|
||||||
|
this.__wrappingDialogNode.show();
|
||||||
|
}
|
||||||
|
// @ts-ignore
|
||||||
|
this.__wrappingDialogNode.style.display = '';
|
||||||
this._keepBodySize({ phase: 'before-show' });
|
this._keepBodySize({ phase: 'before-show' });
|
||||||
await this._handleFeatures({ phase: 'show' });
|
await this._handleFeatures({ phase: 'show' });
|
||||||
this._keepBodySize({ phase: 'show' });
|
this._keepBodySize({ phase: 'show' });
|
||||||
|
|
@ -739,11 +788,17 @@ export class OverlayController extends EventTargetShim {
|
||||||
*/
|
*/
|
||||||
async _handlePosition({ phase }) {
|
async _handlePosition({ phase }) {
|
||||||
if (this.placementMode === 'global') {
|
if (this.placementMode === 'global') {
|
||||||
const addOrRemove = phase === 'show' ? 'add' : 'remove';
|
const placementClass = `global-overlays__overlay-container--${this.viewportConfig.placement}`;
|
||||||
const placementClass = `${GLOBAL_OVERLAYS_CONTAINER_CLASS}--${this.viewportConfig.placement}`;
|
|
||||||
this.contentWrapperNode.classList[addOrRemove](GLOBAL_OVERLAYS_CONTAINER_CLASS);
|
if (phase === 'show') {
|
||||||
this.contentWrapperNode.classList[addOrRemove](placementClass);
|
this.contentWrapperNode.classList.add('global-overlays__overlay-container');
|
||||||
this.contentNode.classList[addOrRemove](GLOBAL_OVERLAYS_CLASS);
|
this.contentWrapperNode.classList.add(placementClass);
|
||||||
|
this.contentNode.classList.add('global-overlays__overlay');
|
||||||
|
} else if (phase === 'hide') {
|
||||||
|
this.contentWrapperNode.classList.remove('global-overlays__overlay-container');
|
||||||
|
this.contentWrapperNode.classList.remove(placementClass);
|
||||||
|
this.contentNode.classList.remove('global-overlays__overlay');
|
||||||
|
}
|
||||||
} else if (this.placementMode === 'local' && phase === 'show') {
|
} else if (this.placementMode === 'local' && phase === 'show') {
|
||||||
/**
|
/**
|
||||||
* Popper is weird about properly positioning the popper element when it is recreated so
|
* Popper is weird about properly positioning the popper element when it is recreated so
|
||||||
|
|
@ -753,7 +808,7 @@ export class OverlayController extends EventTargetShim {
|
||||||
* This is however necessary for initial placement.
|
* This is however necessary for initial placement.
|
||||||
*/
|
*/
|
||||||
await this.__createPopperInstance();
|
await this.__createPopperInstance();
|
||||||
/** @type {Popper} */ (this._popper).forceUpdate();
|
this._popper.forceUpdate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -829,7 +884,11 @@ export class OverlayController extends EventTargetShim {
|
||||||
contentNode: this.contentNode,
|
contentNode: this.contentNode,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.contentWrapperNode.style.display = 'none';
|
if (this.__wrappingDialogNode instanceof HTMLDialogElement) {
|
||||||
|
this.__wrappingDialogNode.close();
|
||||||
|
}
|
||||||
|
// @ts-ignore
|
||||||
|
this.__wrappingDialogNode.style.display = 'none';
|
||||||
this._handleFeatures({ phase: 'hide' });
|
this._handleFeatures({ phase: 'hide' });
|
||||||
this._keepBodySize({ phase: 'hide' });
|
this._keepBodySize({ phase: 'hide' });
|
||||||
this.dispatchEvent(new Event('hide'));
|
this.dispatchEvent(new Event('hide'));
|
||||||
|
|
@ -843,6 +902,7 @@ export class OverlayController extends EventTargetShim {
|
||||||
*
|
*
|
||||||
* @param {{backdropNode:HTMLElement, contentNode:HTMLElement}} hideConfig
|
* @param {{backdropNode:HTMLElement, contentNode:HTMLElement}} hideConfig
|
||||||
*/
|
*/
|
||||||
|
// @ts-ignore
|
||||||
// eslint-disable-next-line class-methods-use-this, no-empty-function, no-unused-vars
|
// eslint-disable-next-line class-methods-use-this, no-empty-function, no-unused-vars
|
||||||
async transitionHide(hideConfig) {}
|
async transitionHide(hideConfig) {}
|
||||||
|
|
||||||
|
|
@ -851,48 +911,27 @@ export class OverlayController extends EventTargetShim {
|
||||||
* @protected
|
* @protected
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line class-methods-use-this, no-empty-function, no-unused-vars
|
// eslint-disable-next-line class-methods-use-this, no-empty-function, no-unused-vars
|
||||||
async _transitionHide(hideConfig) {
|
async _transitionHide({ backdropNode, contentNode }) {
|
||||||
// `this.transitionHide` is a hook for our users
|
// `this.transitionHide` is a hook for our users
|
||||||
await this.transitionHide({ backdropNode: this.backdropNode, contentNode: this.contentNode });
|
await this.transitionHide({ backdropNode, contentNode });
|
||||||
|
this._handlePosition({ phase: 'hide' });
|
||||||
if (hideConfig.backdropNode) {
|
if (!backdropNode) {
|
||||||
hideConfig.backdropNode.classList.remove(
|
return;
|
||||||
`${this.placementMode}-overlays__backdrop--animation-in`,
|
|
||||||
);
|
|
||||||
/** @type {() => void} */
|
|
||||||
let afterFadeOut = () => {};
|
|
||||||
hideConfig.backdropNode.classList.add(
|
|
||||||
`${this.placementMode}-overlays__backdrop--animation-out`,
|
|
||||||
);
|
|
||||||
this.__backdropAnimation = new Promise(resolve => {
|
|
||||||
afterFadeOut = () => {
|
|
||||||
if (hideConfig.backdropNode) {
|
|
||||||
hideConfig.backdropNode.classList.remove(
|
|
||||||
`${this.placementMode}-overlays__backdrop--animation-out`,
|
|
||||||
);
|
|
||||||
hideConfig.backdropNode.classList.remove(
|
|
||||||
`${this.placementMode}-overlays__backdrop--visible`,
|
|
||||||
);
|
|
||||||
hideConfig.backdropNode.removeEventListener('animationend', afterFadeOut);
|
|
||||||
}
|
|
||||||
resolve(undefined);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
hideConfig.backdropNode.addEventListener('animationend', afterFadeOut);
|
|
||||||
}
|
}
|
||||||
|
backdropNode.classList.remove(`global-overlays__backdrop--animation-in`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* To be overridden by subclassers
|
* To be overridden by subclassers
|
||||||
*
|
*
|
||||||
* @param {{backdropNode:HTMLElement, contentNode:HTMLElement}} showConfig
|
* @param {{backdropNode:HTMLElement; contentNode:HTMLElement}} showConfig
|
||||||
*/
|
*/
|
||||||
|
// @ts-ignore
|
||||||
// eslint-disable-next-line class-methods-use-this, no-empty-function, no-unused-vars
|
// eslint-disable-next-line class-methods-use-this, no-empty-function, no-unused-vars
|
||||||
async transitionShow(showConfig) {}
|
async transitionShow(showConfig) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {{backdropNode:HTMLElement, contentNode:HTMLElement}} showConfig
|
* @param {{backdropNode:HTMLElement; contentNode:HTMLElement}} showConfig
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line class-methods-use-this, no-empty-function, no-unused-vars
|
// eslint-disable-next-line class-methods-use-this, no-empty-function, no-unused-vars
|
||||||
async _transitionShow(showConfig) {
|
async _transitionShow(showConfig) {
|
||||||
|
|
@ -900,23 +939,16 @@ export class OverlayController extends EventTargetShim {
|
||||||
await this.transitionShow({ backdropNode: this.backdropNode, contentNode: this.contentNode });
|
await this.transitionShow({ backdropNode: this.backdropNode, contentNode: this.contentNode });
|
||||||
|
|
||||||
if (showConfig.backdropNode) {
|
if (showConfig.backdropNode) {
|
||||||
showConfig.backdropNode.classList.add(
|
showConfig.backdropNode.classList.add(`global-overlays__backdrop--animation-in`);
|
||||||
`${this.placementMode}-overlays__backdrop--animation-in`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @protected */
|
/** @protected */
|
||||||
_restoreFocus() {
|
_restoreFocus() {
|
||||||
const { activeElement } = /** @type {* & ShadowRoot} */ (
|
const { activeElement } = /** @type {ShadowRoot} */ (this.contentWrapperNode.getRootNode());
|
||||||
this.__contentWrapperNode
|
|
||||||
).getRootNode();
|
|
||||||
// We only are allowed to move focus if we (still) 'own' it.
|
// 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
|
||||||
if (
|
if (activeElement instanceof HTMLElement && this.contentWrapperNode.contains(activeElement)) {
|
||||||
activeElement &&
|
|
||||||
/** @type {HTMLElement} */ (this.__contentWrapperNode).contains(activeElement)
|
|
||||||
) {
|
|
||||||
if (this.elementToFocusAfterHide) {
|
if (this.elementToFocusAfterHide) {
|
||||||
this.elementToFocusAfterHide.focus();
|
this.elementToFocusAfterHide.focus();
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -1010,55 +1042,28 @@ export class OverlayController extends EventTargetShim {
|
||||||
* @protected
|
* @protected
|
||||||
*/
|
*/
|
||||||
_handleBackdrop({ phase }) {
|
_handleBackdrop({ phase }) {
|
||||||
|
// eslint-disable-next-line default-case
|
||||||
switch (phase) {
|
switch (phase) {
|
||||||
case 'init': {
|
case 'init': {
|
||||||
if (!this.backdropNode) {
|
if (!this.__backdropInitialized) {
|
||||||
|
if (!this.config?.backdropNode) {
|
||||||
this.__backdropNode = document.createElement('div');
|
this.__backdropNode = document.createElement('div');
|
||||||
/** @type {HTMLElement} */
|
// If backdropNode existed in config, styles are applied by implementing party
|
||||||
(this.backdropNode).slot = 'backdrop';
|
this.__backdropNode.classList.add(`global-overlays__backdrop`);
|
||||||
/** @type {HTMLElement} */
|
|
||||||
(this.backdropNode).classList.add(`${this.placementMode}-overlays__backdrop`);
|
|
||||||
}
|
}
|
||||||
|
// @ts-ignore
|
||||||
let insertionAnchor = /** @type {HTMLElement} */ (this.contentNode.parentNode);
|
this.__wrappingDialogNode.prepend(this.backdropNode);
|
||||||
let insertionBefore = this.contentNode;
|
this.__backdropInitialized = true;
|
||||||
if (this.placementMode === 'global') {
|
|
||||||
insertionAnchor = /** @type {HTMLElement} */ (this.contentWrapperNode.parentElement);
|
|
||||||
insertionBefore = this.contentWrapperNode;
|
|
||||||
}
|
}
|
||||||
insertionAnchor.insertBefore(this.backdropNode, insertionBefore);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'show':
|
case 'show':
|
||||||
this.backdropNode.classList.add(`${this.placementMode}-overlays__backdrop--visible`);
|
this.backdropNode.classList.add(`global-overlays__backdrop--visible`);
|
||||||
this.__hasActiveBackdrop = true;
|
this.__hasActiveBackdrop = true;
|
||||||
break;
|
break;
|
||||||
case 'hide':
|
case 'hide':
|
||||||
if (!this.backdropNode) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.__hasActiveBackdrop = false;
|
this.__hasActiveBackdrop = false;
|
||||||
break;
|
break;
|
||||||
case 'teardown':
|
|
||||||
if (!this.backdropNode || !this.backdropNode.parentNode) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (this.__backdropAnimation) {
|
|
||||||
this.__backdropNodeToBeTornDown = this.backdropNode;
|
|
||||||
|
|
||||||
this.__backdropAnimation.then(() => {
|
|
||||||
if (this.__backdropNodeToBeTornDown && this.__backdropNodeToBeTornDown.parentNode) {
|
|
||||||
this.__backdropNodeToBeTornDown.parentNode.removeChild(
|
|
||||||
this.__backdropNodeToBeTornDown,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.backdropNode.parentNode.removeChild(this.backdropNode);
|
|
||||||
}
|
|
||||||
this.__backdropNode = undefined;
|
|
||||||
break;
|
|
||||||
/* no default */
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1072,7 +1077,16 @@ export class OverlayController extends EventTargetShim {
|
||||||
*/
|
*/
|
||||||
_handleTrapsKeyboardFocus({ phase }) {
|
_handleTrapsKeyboardFocus({ phase }) {
|
||||||
if (phase === 'show') {
|
if (phase === 'show') {
|
||||||
|
// @ts-ignore
|
||||||
|
if ('showModal' in this.__wrappingDialogNode) {
|
||||||
|
// @ts-ignore
|
||||||
|
this.__wrappingDialogNode.close();
|
||||||
|
// @ts-ignore
|
||||||
|
this.__wrappingDialogNode.showModal();
|
||||||
|
}
|
||||||
|
// else {
|
||||||
this.enableTrapsKeyboardFocus();
|
this.enableTrapsKeyboardFocus();
|
||||||
|
// }
|
||||||
} else if (phase === 'hide' || phase === 'teardown') {
|
} else if (phase === 'hide' || phase === 'teardown') {
|
||||||
this.disableTrapsKeyboardFocus();
|
this.disableTrapsKeyboardFocus();
|
||||||
}
|
}
|
||||||
|
|
@ -1271,24 +1285,6 @@ export class OverlayController extends EventTargetShim {
|
||||||
|
|
||||||
teardown() {
|
teardown() {
|
||||||
this._handleFeatures({ phase: 'teardown' });
|
this._handleFeatures({ phase: 'teardown' });
|
||||||
|
|
||||||
if (this.placementMode === 'global' && this.__isContentNodeProjected) {
|
|
||||||
/** @type {HTMLElement} */ (this.__originalContentParent).appendChild(this.contentNode);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove the content node wrapper from the global rootnode
|
|
||||||
this._teardownContentWrapperNode();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @protected */
|
|
||||||
_teardownContentWrapperNode() {
|
|
||||||
if (
|
|
||||||
this.placementMode === 'global' &&
|
|
||||||
this.contentWrapperNode &&
|
|
||||||
this.contentWrapperNode.parentNode
|
|
||||||
) {
|
|
||||||
this.contentWrapperNode.parentNode.removeChild(this.contentWrapperNode);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @private */
|
/** @private */
|
||||||
|
|
|
||||||
|
|
@ -27,9 +27,8 @@ export const OverlayMixinImplementation = superclass =>
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.opened = false;
|
this.opened = false;
|
||||||
/** @private */
|
|
||||||
this.__needsSetup = true;
|
/** @type {Partial<OverlayConfig>} */
|
||||||
/** @type {OverlayConfig} */
|
|
||||||
this.config = {};
|
this.config = {};
|
||||||
|
|
||||||
/** @type {EventListener} */
|
/** @type {EventListener} */
|
||||||
|
|
@ -163,17 +162,14 @@ export const OverlayMixinImplementation = superclass =>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback() {
|
/**
|
||||||
super.connectedCallback();
|
* @param {import('lit-element').PropertyValues } changedProperties
|
||||||
// we do a setup after every connectedCallback as firstUpdated will only be called once
|
*/
|
||||||
this.__needsSetup = true;
|
firstUpdated(changedProperties) {
|
||||||
this.updateComplete.then(() => {
|
super.firstUpdated(changedProperties);
|
||||||
if (this.__needsSetup) {
|
|
||||||
this._setupOverlayCtrl();
|
this._setupOverlayCtrl();
|
||||||
}
|
}
|
||||||
this.__needsSetup = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
disconnectedCallback() {
|
disconnectedCallback() {
|
||||||
super.disconnectedCallback();
|
super.disconnectedCallback();
|
||||||
|
|
@ -197,10 +193,13 @@ export const OverlayMixinImplementation = superclass =>
|
||||||
}
|
}
|
||||||
|
|
||||||
get _overlayBackdropNode() {
|
get _overlayBackdropNode() {
|
||||||
return /** @type {HTMLElement | undefined} */ (
|
if (!this.__cachedOverlayBackdropNode) {
|
||||||
|
this.__cachedOverlayBackdropNode = /** @type {HTMLElement | undefined} */ (
|
||||||
Array.from(this.children).find(child => child.slot === 'backdrop')
|
Array.from(this.children).find(child => child.slot === 'backdrop')
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
return this.__cachedOverlayBackdropNode;
|
||||||
|
}
|
||||||
|
|
||||||
get _overlayContentNode() {
|
get _overlayContentNode() {
|
||||||
if (!this._cachedOverlayContentNode) {
|
if (!this._cachedOverlayContentNode) {
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { globalOverlaysStyle } from './globalOverlaysStyle.js';
|
import { globalOverlaysStyle } from './globalOverlaysStyle.js';
|
||||||
import { setSiblingsInert, unsetSiblingsInert } from './utils/inert-siblings.js';
|
|
||||||
|
|
||||||
// Export this as protected var, so that we can easily mock it in tests
|
// Export this as protected var, so that we can easily mock it in tests
|
||||||
// TODO: combine with browserDetection of core?
|
// TODO: combine with browserDetection of core?
|
||||||
|
|
@ -23,13 +22,6 @@ export const _browserDetection = {
|
||||||
* `OverlaysManager` which manages overlays which are rendered into the body
|
* `OverlaysManager` which manages overlays which are rendered into the body
|
||||||
*/
|
*/
|
||||||
export class OverlaysManager {
|
export class OverlaysManager {
|
||||||
static __createGlobalRootNode() {
|
|
||||||
const rootNode = document.createElement('div');
|
|
||||||
rootNode.classList.add('global-overlays');
|
|
||||||
document.body.appendChild(rootNode);
|
|
||||||
return rootNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
static __createGlobalStyleNode() {
|
static __createGlobalStyleNode() {
|
||||||
const styleTag = document.createElement('style');
|
const styleTag = document.createElement('style');
|
||||||
styleTag.setAttribute('data-global-overlays', '');
|
styleTag.setAttribute('data-global-overlays', '');
|
||||||
|
|
@ -38,19 +30,6 @@ export class OverlaysManager {
|
||||||
return styleTag;
|
return styleTag;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* no setter as .list is intended to be read-only
|
|
||||||
* You can use .add or .remove to modify it
|
|
||||||
*/
|
|
||||||
// eslint-disable-next-line class-methods-use-this
|
|
||||||
get globalRootNode() {
|
|
||||||
if (!OverlaysManager.__globalRootNode) {
|
|
||||||
OverlaysManager.__globalRootNode = OverlaysManager.__createGlobalRootNode();
|
|
||||||
OverlaysManager.__globalStyleNode = OverlaysManager.__createGlobalStyleNode();
|
|
||||||
}
|
|
||||||
return OverlaysManager.__globalRootNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* no setter as .list is intended to be read-only
|
* no setter as .list is intended to be read-only
|
||||||
* You can use .add or .remove to modify it
|
* You can use .add or .remove to modify it
|
||||||
|
|
@ -85,6 +64,10 @@ export class OverlaysManager {
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
this.__blockingMap = new WeakMap();
|
this.__blockingMap = new WeakMap();
|
||||||
|
|
||||||
|
if (!OverlaysManager.__globalStyleNode) {
|
||||||
|
OverlaysManager.__globalStyleNode = OverlaysManager.__createGlobalStyleNode();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -108,6 +91,7 @@ export class OverlaysManager {
|
||||||
throw new Error('could not find controller to remove');
|
throw new Error('could not find controller to remove');
|
||||||
}
|
}
|
||||||
this.__list = this.list.filter(ctrl => ctrl !== ctrlToRemove);
|
this.__list = this.list.filter(ctrl => ctrl !== ctrlToRemove);
|
||||||
|
this.__shownList = this.shownList.filter(ctrl => ctrl !== ctrlToRemove);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -147,13 +131,7 @@ export class OverlaysManager {
|
||||||
this.__shownList = [];
|
this.__shownList = [];
|
||||||
this.__siblingsInert = false;
|
this.__siblingsInert = false;
|
||||||
|
|
||||||
const rootNode = OverlaysManager.__globalRootNode;
|
if (OverlaysManager.__globalStyleNode) {
|
||||||
if (rootNode) {
|
|
||||||
if (rootNode.parentElement) {
|
|
||||||
rootNode.parentElement.removeChild(rootNode);
|
|
||||||
}
|
|
||||||
OverlaysManager.__globalRootNode = undefined;
|
|
||||||
|
|
||||||
document.head.removeChild(
|
document.head.removeChild(
|
||||||
/** @type {HTMLStyleElement} */ (OverlaysManager.__globalStyleNode),
|
/** @type {HTMLStyleElement} */ (OverlaysManager.__globalStyleNode),
|
||||||
);
|
);
|
||||||
|
|
@ -180,9 +158,6 @@ export class OverlaysManager {
|
||||||
*/
|
*/
|
||||||
informTrapsKeyboardFocusGotEnabled(placementMode) {
|
informTrapsKeyboardFocusGotEnabled(placementMode) {
|
||||||
if (this.siblingsInert === false && placementMode === 'global') {
|
if (this.siblingsInert === false && placementMode === 'global') {
|
||||||
if (OverlaysManager.__globalRootNode) {
|
|
||||||
setSiblingsInert(this.globalRootNode);
|
|
||||||
}
|
|
||||||
this.__siblingsInert = true;
|
this.__siblingsInert = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -199,9 +174,6 @@ export class OverlaysManager {
|
||||||
next.enableTrapsKeyboardFocus();
|
next.enableTrapsKeyboardFocus();
|
||||||
}
|
}
|
||||||
} else if (this.siblingsInert === true) {
|
} else if (this.siblingsInert === true) {
|
||||||
if (OverlaysManager.__globalRootNode) {
|
|
||||||
unsetSiblingsInert(this.globalRootNode);
|
|
||||||
}
|
|
||||||
this.__siblingsInert = false;
|
this.__siblingsInert = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -242,8 +214,7 @@ export class OverlaysManager {
|
||||||
*/
|
*/
|
||||||
requestToShowOnly(blockingCtrl) {
|
requestToShowOnly(blockingCtrl) {
|
||||||
const controllersToHide = this.shownList.filter(ctrl => ctrl !== blockingCtrl);
|
const controllersToHide = this.shownList.filter(ctrl => ctrl !== blockingCtrl);
|
||||||
|
controllersToHide.forEach(ctrl => ctrl.hide());
|
||||||
controllersToHide.map(ctrl => ctrl.hide());
|
|
||||||
this.__blockingMap.set(blockingCtrl, controllersToHide);
|
this.__blockingMap.set(blockingCtrl, controllersToHide);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -255,11 +226,10 @@ export class OverlaysManager {
|
||||||
const controllersWhichGotHidden = /** @type {OverlayController[]} */ (
|
const controllersWhichGotHidden = /** @type {OverlayController[]} */ (
|
||||||
this.__blockingMap.get(blockingCtrl)
|
this.__blockingMap.get(blockingCtrl)
|
||||||
);
|
);
|
||||||
controllersWhichGotHidden.map(ctrl => ctrl.show());
|
controllersWhichGotHidden.forEach(ctrl => ctrl.show());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/** @type {HTMLElement | undefined} */
|
|
||||||
OverlaysManager.__globalRootNode = undefined;
|
|
||||||
/** @type {HTMLStyleElement | undefined} */
|
/** @type {HTMLStyleElement | undefined} */
|
||||||
OverlaysManager.__globalStyleNode = undefined;
|
OverlaysManager.__globalStyleNode = undefined;
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,6 @@ export const globalOverlaysStyle = css`
|
||||||
z-index: 200;
|
z-index: 200;
|
||||||
}
|
}
|
||||||
|
|
||||||
.global-overlays__overlay {
|
|
||||||
pointer-events: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.global-overlays__overlay-container {
|
.global-overlays__overlay-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
|
@ -20,6 +16,10 @@ export const globalOverlaysStyle = css`
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.global-overlays__overlay-container::backdrop {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.global-overlays__overlay-container--top-left {
|
.global-overlays__overlay-container--top-left {
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
|
|
@ -54,6 +54,7 @@ export const globalOverlaysStyle = css`
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
.global-overlays__overlay-container--left {
|
.global-overlays__overlay-container--left {
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
@ -68,7 +69,12 @@ export const globalOverlaysStyle = css`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.global-overlays .global-overlays__backdrop {
|
::slotted(.global-overlays__overlay),
|
||||||
|
.global-overlays__overlay {
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.global-overlays__backdrop {
|
||||||
content: '';
|
content: '';
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
|
@ -81,15 +87,16 @@ export const globalOverlaysStyle = css`
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.global-overlays .global-overlays__backdrop--visible {
|
.global-overlays__backdrop--visible {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.global-overlays .global-overlays__backdrop--animation-in {
|
.global-overlays__backdrop--animation-in {
|
||||||
animation: global-overlays-backdrop-fade-in 300ms;
|
animation: global-overlays-backdrop-fade-in 300ms;
|
||||||
|
opacity: 0.3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.global-overlays .global-overlays__backdrop--animation-out {
|
.global-overlays__backdrop--animation-out {
|
||||||
animation: global-overlays-backdrop-fade-out 300ms;
|
animation: global-overlays-backdrop-fade-out 300ms;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
import { singletonManager } from 'singleton-manager';
|
import { singletonManager } from 'singleton-manager';
|
||||||
import { OverlaysManager } from './OverlaysManager.js';
|
import { OverlaysManager } from './OverlaysManager.js';
|
||||||
|
|
||||||
export const overlays = singletonManager.get('@lion/ui::overlays::0.x') || new OverlaysManager();
|
export const overlays =
|
||||||
|
/** @type {OverlaysManager} */
|
||||||
|
(singletonManager.get('@lion/ui::overlays::0.x')) || new OverlaysManager();
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { expect, fixture, html, nextFrame, aTimeout } from '@open-wc/testing';
|
import { expect, fixture, html, nextFrame, aTimeout } from '@open-wc/testing';
|
||||||
|
|
||||||
import sinon from 'sinon';
|
import sinon from 'sinon';
|
||||||
import { overlays, OverlayController } from '@lion/ui/overlays.js';
|
import { overlays as overlaysManager, OverlayController } from '@lion/ui/overlays.js';
|
||||||
|
|
||||||
import '@lion/ui/define/lion-dialog.js';
|
import '@lion/ui/define/lion-dialog.js';
|
||||||
import { _browserDetection } from '../src/OverlaysManager.js';
|
import { _browserDetection } from '../src/OverlaysManager.js';
|
||||||
|
|
@ -15,10 +15,12 @@ import { _browserDetection } from '../src/OverlaysManager.js';
|
||||||
* @typedef {LitElement & OverlayHost & {_overlayCtrl:OverlayController}} OverlayEl
|
* @typedef {LitElement & OverlayHost & {_overlayCtrl:OverlayController}} OverlayEl
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function getGlobalOverlayNodes() {
|
function getGlobalOverlayCtrls() {
|
||||||
return Array.from(overlays.globalRootNode.children).filter(
|
return overlaysManager.list;
|
||||||
child => !child.classList.contains('global-overlays__backdrop'),
|
}
|
||||||
);
|
|
||||||
|
function resetOverlaysManager() {
|
||||||
|
overlaysManager.list.forEach(overlayCtrl => overlaysManager.remove(overlayCtrl));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -220,23 +222,21 @@ export function runOverlayMixinSuite({ tagString, tag, suffix = '' }) {
|
||||||
function sendCloseEvent(/** @type {Event} */ e) {
|
function sendCloseEvent(/** @type {Event} */ e) {
|
||||||
e.target?.dispatchEvent(new Event('close-overlay', { bubbles: true }));
|
e.target?.dispatchEvent(new Event('close-overlay', { bubbles: true }));
|
||||||
}
|
}
|
||||||
const closeBtn = /** @type {OverlayEl} */ (
|
|
||||||
await fixture(html` <button @click=${sendCloseEvent}>close</button> `)
|
|
||||||
);
|
|
||||||
|
|
||||||
const el = /** @type {OverlayEl} */ (
|
const el = /** @type {OverlayEl} */ (
|
||||||
await fixture(html`
|
await fixture(html`
|
||||||
<${tag} opened>
|
<${tag} opened>
|
||||||
<div slot="content">
|
<div slot="content">
|
||||||
content of the overlay
|
content of the overlay
|
||||||
${closeBtn}
|
<button @click=${sendCloseEvent}>close</button>
|
||||||
</div>
|
</div>
|
||||||
<button slot="invoker">invoker button</button>
|
<button slot="invoker">invoker button</button>
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`)
|
`)
|
||||||
);
|
);
|
||||||
closeBtn.click();
|
// @ts-ignore
|
||||||
await nextFrame(); // hide takes at least a frame
|
el.querySelector('[slot=content] button').click();
|
||||||
|
await el._overlayCtrl._hideComplete;
|
||||||
expect(el.opened).to.be.false;
|
expect(el.opened).to.be.false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -337,10 +337,7 @@ export function runOverlayMixinSuite({ tagString, tag, suffix = '' }) {
|
||||||
// For some reason, globalRootNode is not cleared properly on disconnectedCallback from previous overlay test fixtures...
|
// For some reason, globalRootNode is not cleared properly on disconnectedCallback from previous overlay test fixtures...
|
||||||
// Not sure why this "bug" happens...
|
// Not sure why this "bug" happens...
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const globalRootNode = document.querySelector('.global-overlays');
|
resetOverlaysManager();
|
||||||
if (globalRootNode) {
|
|
||||||
globalRootNode.innerHTML = '';
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('supports nested overlays', async () => {
|
it('supports nested overlays', async () => {
|
||||||
|
|
@ -362,7 +359,7 @@ export function runOverlayMixinSuite({ tagString, tag, suffix = '' }) {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (el._overlayCtrl.placementMode === 'global') {
|
if (el._overlayCtrl.placementMode === 'global') {
|
||||||
expect(getGlobalOverlayNodes().length).to.equal(2);
|
expect(getGlobalOverlayCtrls().length).to.equal(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
el.opened = true;
|
el.opened = true;
|
||||||
|
|
@ -386,12 +383,12 @@ export function runOverlayMixinSuite({ tagString, tag, suffix = '' }) {
|
||||||
`)
|
`)
|
||||||
);
|
);
|
||||||
if (el._overlayCtrl.placementMode === 'global') {
|
if (el._overlayCtrl.placementMode === 'global') {
|
||||||
expect(getGlobalOverlayNodes().length).to.equal(1);
|
expect(getGlobalOverlayCtrls().length).to.equal(1);
|
||||||
|
|
||||||
const moveTarget = /** @type {OverlayEl} */ (await fixture('<div id="target"></div>'));
|
const moveTarget = /** @type {OverlayEl} */ (await fixture('<div id="target"></div>'));
|
||||||
moveTarget.appendChild(el);
|
moveTarget.appendChild(el);
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
expect(getGlobalOverlayNodes().length).to.equal(1);
|
expect(getGlobalOverlayCtrls().length).to.equal(1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -419,14 +416,9 @@ export function runOverlayMixinSuite({ tagString, tag, suffix = '' }) {
|
||||||
|
|
||||||
if (el._overlayCtrl.placementMode === 'global') {
|
if (el._overlayCtrl.placementMode === 'global') {
|
||||||
// Find the outlets that are not backdrop outlets
|
// Find the outlets that are not backdrop outlets
|
||||||
const overlayContainerNodes = getGlobalOverlayNodes();
|
expect(getGlobalOverlayCtrls().length).to.equal(2);
|
||||||
expect(overlayContainerNodes.length).to.equal(2);
|
|
||||||
const lastContentNodeInContainer = overlayContainerNodes[1];
|
|
||||||
// Check that the last container is the nested one with the intended content
|
// Check that the last container is the nested one with the intended content
|
||||||
expect(lastContentNodeInContainer.firstElementChild.firstChild.textContent).to.equal(
|
expect(el.contains(nestedEl)).to.be.true;
|
||||||
'content of the nested overlay',
|
|
||||||
);
|
|
||||||
expect(lastContentNodeInContainer.firstElementChild.slot).to.equal('content');
|
|
||||||
} else {
|
} else {
|
||||||
const contentNode = /** @type {HTMLElement} */ (
|
const contentNode = /** @type {HTMLElement} */ (
|
||||||
// @ts-ignore [allow-protected] in tests
|
// @ts-ignore [allow-protected] in tests
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,6 @@ describe('ArrowMixin', () => {
|
||||||
`)
|
`)
|
||||||
);
|
);
|
||||||
expect(el.hasAttribute('has-arrow')).to.be.true;
|
expect(el.hasAttribute('has-arrow')).to.be.true;
|
||||||
|
|
||||||
const arrowNode = /** @type {Element} */ (el._arrowNode);
|
const arrowNode = /** @type {Element} */ (el._arrowNode);
|
||||||
expect(window.getComputedStyle(arrowNode).getPropertyValue('display')).to.equal('block');
|
expect(window.getComputedStyle(arrowNode).getPropertyValue('display')).to.equal('block');
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,18 @@ import { simulateTab } from '../src/utils/simulate-tab.js';
|
||||||
* @typedef {import('../types/OverlayConfig.js').ViewportPlacement} ViewportPlacement
|
* @typedef {import('../types/OverlayConfig.js').ViewportPlacement} ViewportPlacement
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
const wrappingDialogNodeStyle =
|
||||||
|
'display: none; background-image: none; border-style: none; padding: 0px; z-index: 9999;';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {HTMLElement} node
|
||||||
|
*/
|
||||||
|
function normalizeOverlayContentWapper(node) {
|
||||||
|
if (node.hasAttribute('style') && !node.style.cssText) {
|
||||||
|
node.removeAttribute('style');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {OverlayController} overlayControllerEl
|
* @param {OverlayController} overlayControllerEl
|
||||||
*/
|
*/
|
||||||
|
|
@ -66,7 +78,7 @@ describe('OverlayController', () => {
|
||||||
...withGlobalTestConfig(),
|
...withGlobalTestConfig(),
|
||||||
});
|
});
|
||||||
expect(ctrl.content).not.to.be.undefined;
|
expect(ctrl.content).not.to.be.undefined;
|
||||||
expect(ctrl.contentNode.parentElement).to.equal(ctrl.content);
|
expect(ctrl.contentNode.parentElement).to.equal(ctrl.contentWrapperNode);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Z-index on local overlays', () => {
|
describe('Z-index on local overlays', () => {
|
||||||
|
|
@ -106,17 +118,21 @@ describe('OverlayController', () => {
|
||||||
contentNode: await createZNode('auto', { mode: 'global' }),
|
contentNode: await createZNode('auto', { mode: 'global' }),
|
||||||
});
|
});
|
||||||
await ctrl.show();
|
await ctrl.show();
|
||||||
expect(ctrl.content.style.zIndex).to.equal('1');
|
// @ts-expect-error find out why config would/could be undfined
|
||||||
|
expect(ctrl.content.style.zIndex).to.equal(`${ctrl.config.zIndex + 1}`);
|
||||||
ctrl.updateConfig({ contentNode: await createZNode('auto', { mode: 'inline' }) });
|
ctrl.updateConfig({ contentNode: await createZNode('auto', { mode: 'inline' }) });
|
||||||
await ctrl.show();
|
await ctrl.show();
|
||||||
expect(ctrl.content.style.zIndex).to.equal('1');
|
// @ts-expect-error find out why config would/could be undfined
|
||||||
|
expect(ctrl.content.style.zIndex).to.equal(`${ctrl.config.zIndex + 1}`);
|
||||||
|
|
||||||
ctrl.updateConfig({ contentNode: await createZNode('0', { mode: 'global' }) });
|
ctrl.updateConfig({ contentNode: await createZNode('0', { mode: 'global' }) });
|
||||||
await ctrl.show();
|
await ctrl.show();
|
||||||
expect(ctrl.content.style.zIndex).to.equal('1');
|
// @ts-expect-error find out why config would/could be undfined
|
||||||
|
expect(ctrl.content.style.zIndex).to.equal(`${ctrl.config.zIndex + 1}`);
|
||||||
ctrl.updateConfig({ contentNode: await createZNode('0', { mode: 'inline' }) });
|
ctrl.updateConfig({ contentNode: await createZNode('0', { mode: 'inline' }) });
|
||||||
await ctrl.show();
|
await ctrl.show();
|
||||||
expect(ctrl.content.style.zIndex).to.equal('1');
|
// @ts-expect-error find out why config would/could be undfined
|
||||||
|
expect(ctrl.content.style.zIndex).to.equal(`${ctrl.config.zIndex + 1}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it.skip("doesn't set a z-index when contentNode already has >= 1", async () => {
|
it.skip("doesn't set a z-index when contentNode already has >= 1", async () => {
|
||||||
|
|
@ -147,42 +163,7 @@ describe('OverlayController', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Render target', () => {
|
describe('Offline content', () => {
|
||||||
it('creates global target for placement mode "global"', async () => {
|
|
||||||
const ctrl = new OverlayController({
|
|
||||||
...withGlobalTestConfig(),
|
|
||||||
});
|
|
||||||
const { renderTarget } = getProtectedMembers(ctrl);
|
|
||||||
expect(renderTarget).to.equal(overlays.globalRootNode);
|
|
||||||
});
|
|
||||||
|
|
||||||
it.skip('creates local target next to sibling for placement mode "local"', async () => {
|
|
||||||
const ctrl = new OverlayController({
|
|
||||||
...withLocalTestConfig(),
|
|
||||||
invokerNode: /** @type {HTMLElement} */ (await fixture(html`<button>Invoker</button>`)),
|
|
||||||
});
|
|
||||||
const { renderTarget } = getProtectedMembers(ctrl);
|
|
||||||
expect(renderTarget).to.be.undefined;
|
|
||||||
expect(ctrl.content).to.equal(ctrl.invokerNode?.nextElementSibling);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('keeps local target for placement mode "local" when already connected', async () => {
|
|
||||||
const parentNode = /** @type {HTMLElement} */ (
|
|
||||||
await fixture(html`
|
|
||||||
<div id="parent">
|
|
||||||
<div id="content">Content</div>
|
|
||||||
</div>
|
|
||||||
`)
|
|
||||||
);
|
|
||||||
const contentNode = /** @type {HTMLElement} */ (parentNode.querySelector('#content'));
|
|
||||||
const ctrl = new OverlayController({
|
|
||||||
...withLocalTestConfig(),
|
|
||||||
contentNode,
|
|
||||||
});
|
|
||||||
const { renderTarget } = getProtectedMembers(ctrl);
|
|
||||||
expect(renderTarget).to.equal(parentNode);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('throws when passing a content node that was created "offline"', async () => {
|
it('throws when passing a content node that was created "offline"', async () => {
|
||||||
const contentNode = document.createElement('div');
|
const contentNode = document.createElement('div');
|
||||||
const createOverlayController = () => {
|
const createOverlayController = () => {
|
||||||
|
|
@ -203,107 +184,245 @@ describe('OverlayController', () => {
|
||||||
...withLocalTestConfig(),
|
...withLocalTestConfig(),
|
||||||
contentNode,
|
contentNode,
|
||||||
});
|
});
|
||||||
const { renderTarget } = getProtectedMembers(overlay);
|
|
||||||
|
|
||||||
expect(overlay.contentNode.isConnected).to.be.true;
|
expect(overlay.contentNode.isConnected).to.be.true;
|
||||||
expect(renderTarget).to.not.be.undefined;
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: Add teardown feature tests
|
// TODO: Add teardown feature tests
|
||||||
describe('Teardown', () => {
|
describe('Teardown', () => {});
|
||||||
it('removes the contentWrapperNode from global rootnode upon teardown', async () => {
|
|
||||||
const ctrl = new OverlayController({
|
|
||||||
...withGlobalTestConfig(),
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(ctrl.manager.globalRootNode.children.length).to.equal(1);
|
|
||||||
ctrl.teardown();
|
|
||||||
expect(ctrl.manager.globalRootNode.children.length).to.equal(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('[global] restores contentNode if it was/is a projected node', async () => {
|
|
||||||
const shadowHost = document.createElement('div');
|
|
||||||
shadowHost.id = 'shadowHost';
|
|
||||||
shadowHost.attachShadow({ mode: 'open' });
|
|
||||||
/** @type {ShadowRoot} */
|
|
||||||
(shadowHost.shadowRoot).innerHTML = `
|
|
||||||
<div id="contentWrapperNode">
|
|
||||||
<slot name="contentNode"></slot>
|
|
||||||
<my-arrow></my-arrow>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
const contentNode = document.createElement('div');
|
|
||||||
contentNode.slot = 'contentNode';
|
|
||||||
shadowHost.appendChild(contentNode);
|
|
||||||
|
|
||||||
const wrapper = /** @type {HTMLElement} */ (await fixture('<div id="wrapper"></div>'));
|
|
||||||
// Ensure the contentNode is connected to DOM
|
|
||||||
wrapper.appendChild(shadowHost);
|
|
||||||
|
|
||||||
// has one child = <div slot="contentNode"></div>
|
|
||||||
expect(shadowHost.children.length).to.equal(1);
|
|
||||||
|
|
||||||
const ctrl = new OverlayController({
|
|
||||||
...withLocalTestConfig(),
|
|
||||||
placementMode: 'global',
|
|
||||||
contentNode,
|
|
||||||
contentWrapperNode: shadowHost,
|
|
||||||
});
|
|
||||||
|
|
||||||
// has no children as content gets moved to the body
|
|
||||||
expect(shadowHost.children.length).to.equal(0);
|
|
||||||
ctrl.teardown();
|
|
||||||
|
|
||||||
// restores original light dom in teardown
|
|
||||||
expect(shadowHost.children.length).to.equal(1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Node Configuration', () => {
|
describe('Node Configuration', () => {
|
||||||
it('accepts an .contentNode<Node> to directly set content', async () => {
|
describe('Content', async () => {
|
||||||
|
it('accepts a .contentNode for displaying content of the overlay', async () => {
|
||||||
|
const myContentNode = /** @type {HTMLElement} */ (fixtureSync('<p>direct node</p>'));
|
||||||
const ctrl = new OverlayController({
|
const ctrl = new OverlayController({
|
||||||
...withGlobalTestConfig(),
|
...withGlobalTestConfig(),
|
||||||
contentNode: /** @type {HTMLElement} */ (await fixture('<p>direct node</p>')),
|
contentNode: myContentNode,
|
||||||
});
|
});
|
||||||
expect(ctrl.contentNode).to.have.trimmed.text('direct node');
|
expect(ctrl.contentNode).to.have.trimmed.text('direct node');
|
||||||
|
expect(ctrl.contentNode).to.equal(myContentNode);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('accepts an .invokerNode<Node> to directly set invoker', async () => {
|
describe('Embedded dom structure', async () => {
|
||||||
|
describe('When projected in shadow dom', async () => {
|
||||||
|
it('wraps a .contentWrapperNode for style application and a <dialog role="none"> for top layer paints', async () => {
|
||||||
|
const tagString = defineCE(
|
||||||
|
class extends HTMLElement {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.attachShadow({ mode: 'open' });
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
/** @type {ShadowRoot} */
|
||||||
|
(this.shadowRoot).innerHTML = '<slot name="content"></slot>';
|
||||||
|
this.innerHTML = '<div slot="content">projected</div>';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const el = /** @type {HTMLElement} */ (fixtureSync(`<${tagString}></${tagString}>`));
|
||||||
|
const myContentNode = /** @type {HTMLElement} */ (el.querySelector('[slot=content]'));
|
||||||
const ctrl = new OverlayController({
|
const ctrl = new OverlayController({
|
||||||
...withGlobalTestConfig(),
|
...withGlobalTestConfig(),
|
||||||
invokerNode: /** @type {HTMLElement} */ (await fixture('<button>invoke</button>')),
|
contentNode: myContentNode,
|
||||||
});
|
|
||||||
expect(ctrl.invokerNode).to.have.trimmed.text('invoke');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('When contentWrapperNode projects contentNode', () => {
|
expect(ctrl.contentNode.assignedSlot?.parentElement).to.equal(ctrl.contentWrapperNode);
|
||||||
it('recognizes projected contentNode', async () => {
|
expect(ctrl.contentWrapperNode.parentElement?.tagName).to.equal('DIALOG');
|
||||||
const shadowHost = document.createElement('div');
|
|
||||||
shadowHost.attachShadow({ mode: 'open' });
|
normalizeOverlayContentWapper(ctrl.contentWrapperNode);
|
||||||
/** @type {ShadowRoot} */ (shadowHost.shadowRoot).innerHTML = `
|
|
||||||
<div id="contentWrapperNode">
|
// The total dom structure created...
|
||||||
<slot name="contentNode"></slot>
|
expect(el).shadowDom.to.equal(`
|
||||||
<my-arrow></my-arrow>
|
<dialog open="" role="none" style="${wrappingDialogNodeStyle}">
|
||||||
|
<div data-id="content-wrapper">
|
||||||
|
<slot name="content">
|
||||||
|
</slot>
|
||||||
</div>
|
</div>
|
||||||
`;
|
</dialog>
|
||||||
const contentNode = document.createElement('div');
|
`);
|
||||||
contentNode.slot = 'contentNode';
|
|
||||||
shadowHost.appendChild(contentNode);
|
|
||||||
|
|
||||||
// Ensure the contentNode is connected to DOM
|
expect(el).lightDom.to.equal(`<div slot="content">projected</div>`);
|
||||||
document.body.appendChild(shadowHost);
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('When in light dom', async () => {
|
||||||
|
it('wraps a .contentWrapperNode for style application and a <dialog role="none"> for top layer paints', async () => {
|
||||||
|
const el = fixtureSync('<section><div id="content">non projected</div></section>');
|
||||||
|
const myContentNode = /** @type {HTMLElement} */ (el.querySelector('#content'));
|
||||||
|
const ctrl = new OverlayController({
|
||||||
|
...withGlobalTestConfig(),
|
||||||
|
contentNode: myContentNode,
|
||||||
|
});
|
||||||
|
expect(ctrl.contentNode.parentElement).to.equal(ctrl.contentWrapperNode);
|
||||||
|
expect(ctrl.contentWrapperNode.parentElement?.tagName).to.equal('DIALOG');
|
||||||
|
|
||||||
|
normalizeOverlayContentWapper(ctrl.contentWrapperNode);
|
||||||
|
|
||||||
|
// The total dom structure created...
|
||||||
|
expect(el).lightDom.to.equal(`
|
||||||
|
<dialog open="" role="none" style="${wrappingDialogNodeStyle}">
|
||||||
|
<div data-id="content-wrapper">
|
||||||
|
<div id="content">non projected</div>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('When .contenWrapperNode provided', async () => {
|
||||||
|
it('keeps the .contentWrapperNode for style application and wraps a <dialog role="none"> for top layer paints', async () => {
|
||||||
|
const tagString = defineCE(
|
||||||
|
class extends HTMLElement {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.attachShadow({ mode: 'open' });
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
/** @type {ShadowRoot} */
|
||||||
|
(this.shadowRoot).innerHTML = '<div><slot name="content"></slot></div>';
|
||||||
|
this.innerHTML = '<div slot="content">projected</div>';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const el = /** @type {HTMLElement} */ (fixtureSync(`<${tagString}></${tagString}>`));
|
||||||
|
const myContentNode = /** @type {HTMLElement} */ (el.querySelector('[slot=content]'));
|
||||||
|
const myContentWrapper = /** @type {HTMLElement} */ (
|
||||||
|
el.shadowRoot?.querySelector('div')
|
||||||
|
);
|
||||||
|
const ctrl = new OverlayController({
|
||||||
|
...withGlobalTestConfig(),
|
||||||
|
contentNode: myContentNode,
|
||||||
|
contentWrapperNode: myContentWrapper,
|
||||||
|
});
|
||||||
|
|
||||||
|
normalizeOverlayContentWapper(ctrl.contentWrapperNode);
|
||||||
|
|
||||||
|
// The total dom structure created...
|
||||||
|
expect(el).shadowDom.to.equal(`
|
||||||
|
<dialog open="" role="none" style="${wrappingDialogNodeStyle}">
|
||||||
|
<div data-id="content-wrapper">
|
||||||
|
<slot name="content"></slot>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses the .contentWrapperNode as container for Popper's arrow", async () => {
|
||||||
|
const tagString = defineCE(
|
||||||
|
class extends HTMLElement {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.attachShadow({ mode: 'open' });
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
/** @type {ShadowRoot} */
|
||||||
|
(this.shadowRoot).innerHTML = `
|
||||||
|
<div>
|
||||||
|
<div id="arrow"></div>
|
||||||
|
<slot name="content"></slot>
|
||||||
|
</div>`;
|
||||||
|
this.innerHTML = '<div slot="content">projected</div>';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const el = /** @type {HTMLElement} */ (fixtureSync(`<${tagString}></${tagString}>`));
|
||||||
|
const myContentNode = /** @type {HTMLElement} */ (el.querySelector('[slot=content]'));
|
||||||
|
const myContentWrapper = /** @type {HTMLElement} */ (
|
||||||
|
el.shadowRoot?.querySelector('div')
|
||||||
|
);
|
||||||
const ctrl = new OverlayController({
|
const ctrl = new OverlayController({
|
||||||
...withLocalTestConfig(),
|
...withLocalTestConfig(),
|
||||||
contentNode,
|
contentNode: myContentNode,
|
||||||
contentWrapperNode: /** @type {HTMLElement} */ (
|
contentWrapperNode: myContentWrapper,
|
||||||
/** @type {ShadowRoot} */ (shadowHost.shadowRoot).getElementById('contentWrapperNode')
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(ctrl.__isContentNodeProjected).to.be.true;
|
normalizeOverlayContentWapper(ctrl.contentWrapperNode);
|
||||||
|
|
||||||
|
// The total dom structure created...
|
||||||
|
expect(el).shadowDom.to.equal(`
|
||||||
|
<dialog open="" role="none" style="${wrappingDialogNodeStyle}">
|
||||||
|
<div data-id="content-wrapper">
|
||||||
|
<div id="arrow"></div>
|
||||||
|
<slot name="content"></slot>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Invoker / Reference', async () => {
|
||||||
|
it('accepts a .invokerNode to directly set invoker', async () => {
|
||||||
|
const myInvokerNode = /** @type {HTMLElement} */ (fixtureSync('<button>invoke</button>'));
|
||||||
|
const ctrl = new OverlayController({
|
||||||
|
...withGlobalTestConfig(),
|
||||||
|
invokerNode: myInvokerNode,
|
||||||
|
});
|
||||||
|
expect(ctrl.invokerNode).to.equal(myInvokerNode);
|
||||||
|
expect(ctrl.referenceNode).to.equal(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accepts a .referenceNode as positioning anchor different from .invokerNode', async () => {
|
||||||
|
const myInvokerNode = /** @type {HTMLElement} */ (fixtureSync('<button>invoke</button>'));
|
||||||
|
const myReferenceNode = /** @type {HTMLElement} */ (fixtureSync('<div>anchor</div>'));
|
||||||
|
const ctrl = new OverlayController({
|
||||||
|
...withGlobalTestConfig(),
|
||||||
|
invokerNode: myInvokerNode,
|
||||||
|
referenceNode: myReferenceNode,
|
||||||
|
});
|
||||||
|
expect(ctrl.referenceNode).to.equal(myReferenceNode);
|
||||||
|
expect(ctrl.invokerNode).to.not.equal(ctrl.referenceNode);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Backdrop', () => {
|
||||||
|
it('creates a .backdropNode inside <dialog> for guaranteed top layer paints and positioning opportunities', async () => {
|
||||||
|
const tagString = defineCE(
|
||||||
|
class extends HTMLElement {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.attachShadow({ mode: 'open' });
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
/** @type {ShadowRoot} */
|
||||||
|
(this.shadowRoot).innerHTML = '<slot name="content"></slot>';
|
||||||
|
this.innerHTML = '<div slot="content">projected</div>';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const el = /** @type {HTMLElement} */ (fixtureSync(`<${tagString}></${tagString}>`));
|
||||||
|
const myContentNode = /** @type {HTMLElement} */ (el.querySelector('[slot=content]'));
|
||||||
|
|
||||||
|
const ctrl = new OverlayController({
|
||||||
|
...withGlobalTestConfig(),
|
||||||
|
contentNode: myContentNode,
|
||||||
|
hasBackdrop: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
normalizeOverlayContentWapper(ctrl.contentWrapperNode);
|
||||||
|
|
||||||
|
// The total dom structure created...
|
||||||
|
expect(el).shadowDom.to.equal(
|
||||||
|
`
|
||||||
|
<dialog open="" role="none" style="${wrappingDialogNodeStyle}">
|
||||||
|
<div class="global-overlays__backdrop"></div>
|
||||||
|
<div data-id="content-wrapper">
|
||||||
|
<slot name="content">
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
`,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -1003,24 +1122,24 @@ describe('OverlayController', () => {
|
||||||
await ctrl0.show();
|
await ctrl0.show();
|
||||||
await ctrl1.show();
|
await ctrl1.show();
|
||||||
await ctrl2.show(); // blocking
|
await ctrl2.show(); // blocking
|
||||||
expect(ctrl0.content).to.not.be.displayed;
|
expect(ctrl0.__wrappingDialogNode).to.not.be.displayed;
|
||||||
expect(ctrl1.content).to.not.be.displayed;
|
expect(ctrl1.__wrappingDialogNode).to.not.be.displayed;
|
||||||
expect(ctrl2.content).to.be.displayed;
|
expect(ctrl2.__wrappingDialogNode).to.be.displayed;
|
||||||
|
|
||||||
await ctrl3.show();
|
await ctrl3.show();
|
||||||
await ctrl3._showComplete;
|
await ctrl3._showComplete;
|
||||||
expect(ctrl3.content).to.be.displayed;
|
expect(ctrl3.__wrappingDialogNode).to.be.displayed;
|
||||||
|
|
||||||
await ctrl2.hide();
|
await ctrl2.hide();
|
||||||
await ctrl2._hideComplete;
|
await ctrl2._hideComplete;
|
||||||
expect(ctrl0.content).to.be.displayed;
|
expect(ctrl0.__wrappingDialogNode).to.be.displayed;
|
||||||
expect(ctrl1.content).to.be.displayed;
|
expect(ctrl1.__wrappingDialogNode).to.be.displayed;
|
||||||
|
|
||||||
await ctrl2.show(); // blocking
|
await ctrl2.show(); // blocking
|
||||||
expect(ctrl0.content).to.not.be.displayed;
|
expect(ctrl0.__wrappingDialogNode).to.not.be.displayed;
|
||||||
expect(ctrl1.content).to.not.be.displayed;
|
expect(ctrl1.__wrappingDialogNode).to.not.be.displayed;
|
||||||
expect(ctrl2.content).to.be.displayed;
|
expect(ctrl2.__wrappingDialogNode).to.be.displayed;
|
||||||
expect(ctrl3.content).to.not.be.displayed;
|
expect(ctrl3.__wrappingDialogNode).to.not.be.displayed;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('keeps backdrop status when used in combination with blocking', async () => {
|
it('keeps backdrop status when used in combination with blocking', async () => {
|
||||||
|
|
@ -1197,11 +1316,11 @@ describe('OverlayController', () => {
|
||||||
|
|
||||||
ctrl.hide();
|
ctrl.hide();
|
||||||
|
|
||||||
expect(getComputedStyle(ctrl.contentWrapperNode).display).to.equal('block');
|
expect(getComputedStyle(ctrl.content).display).to.equal('block');
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
hideTransitionFinished();
|
hideTransitionFinished();
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
expect(getComputedStyle(ctrl.contentWrapperNode).display).to.equal('none');
|
expect(getComputedStyle(ctrl.content).display).to.equal('none');
|
||||||
done();
|
done();
|
||||||
}, 0);
|
}, 0);
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
@ -1379,89 +1498,9 @@ describe('OverlayController', () => {
|
||||||
expect(properlyInstantiated).to.be.true;
|
expect(properlyInstantiated).to.be.true;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('adds attributes inert and aria-hidden="true" on all siblings of rootNode if an overlay is shown', async () => {
|
// TODO: check if we covered all functionality. "Inertness" should be handled by the platform with a modal overlay...
|
||||||
const ctrl = new OverlayController({
|
it.skip('adds attributes inert and aria-hidden="true" on all siblings of rootNode if an overlay is shown', async () => {});
|
||||||
...withGlobalTestConfig(),
|
it.skip('disables pointer events and selection on inert elements', async () => {});
|
||||||
trapsKeyboardFocus: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const sibling1 = document.createElement('div');
|
|
||||||
const sibling2 = document.createElement('div');
|
|
||||||
document.body.insertBefore(sibling1, ctrl.manager.globalRootNode);
|
|
||||||
document.body.appendChild(sibling2);
|
|
||||||
|
|
||||||
await ctrl.show();
|
|
||||||
|
|
||||||
[sibling1, sibling2].forEach(sibling => {
|
|
||||||
expect(sibling).to.have.attribute('aria-hidden', 'true');
|
|
||||||
expect(sibling).to.have.attribute('inert');
|
|
||||||
});
|
|
||||||
expect(ctrl.content.hasAttribute('aria-hidden')).to.be.false;
|
|
||||||
expect(ctrl.content.hasAttribute('inert')).to.be.false;
|
|
||||||
|
|
||||||
await ctrl.hide();
|
|
||||||
|
|
||||||
[sibling1, sibling2].forEach(sibling => {
|
|
||||||
expect(sibling).to.not.have.attribute('aria-hidden');
|
|
||||||
expect(sibling).to.not.have.attribute('inert');
|
|
||||||
});
|
|
||||||
|
|
||||||
// cleanup
|
|
||||||
document.body.removeChild(sibling1);
|
|
||||||
document.body.removeChild(sibling2);
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* style.userSelect:
|
|
||||||
* - chrome: 'none'
|
|
||||||
* - rest: undefined
|
|
||||||
*
|
|
||||||
* style.pointerEvents:
|
|
||||||
* - chrome: auto
|
|
||||||
* - IE11: visiblePainted
|
|
||||||
*/
|
|
||||||
it('disables pointer events and selection on inert elements', async () => {
|
|
||||||
const ctrl = new OverlayController({
|
|
||||||
...withGlobalTestConfig(),
|
|
||||||
trapsKeyboardFocus: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
// show+hide are needed to create a root node
|
|
||||||
await ctrl.show();
|
|
||||||
await ctrl.hide();
|
|
||||||
|
|
||||||
const sibling1 = document.createElement('div');
|
|
||||||
const sibling2 = document.createElement('div');
|
|
||||||
document.body.insertBefore(sibling1, ctrl.manager.globalRootNode);
|
|
||||||
document.body.appendChild(sibling2);
|
|
||||||
|
|
||||||
await ctrl.show();
|
|
||||||
|
|
||||||
[sibling1, sibling2].forEach(sibling => {
|
|
||||||
expect(window.getComputedStyle(sibling).userSelect).to.be.oneOf(['none', undefined]);
|
|
||||||
expect(window.getComputedStyle(sibling).pointerEvents).to.equal('none');
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(window.getComputedStyle(ctrl.contentNode).userSelect).to.be.oneOf(['auto', undefined]);
|
|
||||||
expect(window.getComputedStyle(ctrl.contentNode).pointerEvents).to.be.oneOf([
|
|
||||||
'auto',
|
|
||||||
'visiblePainted',
|
|
||||||
]);
|
|
||||||
|
|
||||||
await ctrl.hide();
|
|
||||||
|
|
||||||
[sibling1, sibling2].forEach(sibling => {
|
|
||||||
expect(window.getComputedStyle(sibling).userSelect).to.be.oneOf(['auto', undefined]);
|
|
||||||
expect(window.getComputedStyle(sibling).pointerEvents).to.be.oneOf([
|
|
||||||
'auto',
|
|
||||||
'visiblePainted',
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
// cleanup
|
|
||||||
document.body.removeChild(sibling1);
|
|
||||||
document.body.removeChild(sibling2);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Tooltip', () => {
|
describe('Tooltip', () => {
|
||||||
it('adds [aria-describedby] on invoker', async () => {
|
it('adds [aria-describedby] on invoker', async () => {
|
||||||
|
|
@ -1616,47 +1655,6 @@ describe('OverlayController', () => {
|
||||||
}).to.throw('[OverlayController] You need to provide a .contentNode');
|
}).to.throw('[OverlayController] You need to provide a .contentNode');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws if contentNodeWrapper is not provided for projected contentNode', async () => {
|
|
||||||
const shadowHost = document.createElement('div');
|
|
||||||
shadowHost.attachShadow({ mode: 'open' });
|
|
||||||
/** @type {ShadowRoot} */ (shadowHost.shadowRoot).innerHTML = `
|
|
||||||
<div id="contentWrapperNode">
|
|
||||||
<slot name="contentNode"></slot>
|
|
||||||
<my-arrow></my-arrow>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
const contentNode = document.createElement('div');
|
|
||||||
contentNode.slot = 'contentNode';
|
|
||||||
shadowHost.appendChild(contentNode);
|
|
||||||
|
|
||||||
// Ensure the contentNode is connected to DOM
|
|
||||||
document.body.appendChild(shadowHost);
|
|
||||||
|
|
||||||
expect(() => {
|
|
||||||
new OverlayController({
|
|
||||||
...withLocalTestConfig(),
|
|
||||||
contentNode,
|
|
||||||
});
|
|
||||||
}).to.throw(
|
|
||||||
'[OverlayController] You need to provide a .contentWrapperNode when .contentNode is projected',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('throws if placementMode is global for a tooltip', async () => {
|
|
||||||
const contentNode = document.createElement('div');
|
|
||||||
document.body.appendChild(contentNode);
|
|
||||||
expect(() => {
|
|
||||||
new OverlayController({
|
|
||||||
placementMode: 'global',
|
|
||||||
contentNode,
|
|
||||||
isTooltip: true,
|
|
||||||
handlesAccessibility: true,
|
|
||||||
});
|
|
||||||
}).to.throw(
|
|
||||||
'[OverlayController] .isTooltip should be configured with .placementMode "local"',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('throws if handlesAccessibility is false for a tooltip', async () => {
|
it('throws if handlesAccessibility is false for a tooltip', async () => {
|
||||||
const contentNode = document.createElement('div');
|
const contentNode = document.createElement('div');
|
||||||
document.body.appendChild(contentNode);
|
document.body.appendChild(contentNode);
|
||||||
|
|
|
||||||
|
|
@ -27,24 +27,25 @@ describe('OverlaysManager', () => {
|
||||||
mngr.teardown();
|
mngr.teardown();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('provides .globalRootNode as a render target on first access', () => {
|
it('provides global stylesheet for arrangement of body scroll', () => {
|
||||||
expect(document.body.querySelectorAll('.global-overlays').length).to.equal(0);
|
expect(document.head.querySelectorAll('[data-global-overlays]').length).to.equal(1);
|
||||||
const rootNode = mngr.globalRootNode;
|
|
||||||
expect(document.body.querySelector('.global-overlays')).to.equal(rootNode);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('provides .teardown() for cleanup', () => {
|
it('provides .teardown() for cleanup', () => {
|
||||||
const rootNode = mngr.globalRootNode;
|
|
||||||
expect(document.body.querySelector('.global-overlays')).to.equal(rootNode);
|
|
||||||
expect(document.head.querySelector('[data-global-overlays=""]')).not.be.undefined;
|
expect(document.head.querySelector('[data-global-overlays=""]')).not.be.undefined;
|
||||||
|
|
||||||
mngr.teardown();
|
mngr.teardown();
|
||||||
expect(document.body.querySelectorAll('.global-overlays').length).to.equal(0);
|
|
||||||
expect(document.head.querySelector('[data-global-overlays=""]')).be.null;
|
expect(document.head.querySelector('[data-global-overlays=""]')).be.null;
|
||||||
|
|
||||||
// safety check via private access (do not use this)
|
// safety check via private access (do not use this)
|
||||||
expect(OverlaysManager.__globalRootNode).to.be.undefined;
|
|
||||||
expect(OverlaysManager.__globalStyleNode).to.be.undefined;
|
expect(OverlaysManager.__globalStyleNode).to.be.undefined;
|
||||||
|
|
||||||
|
// @ts-ignore [allow-private-in-test]
|
||||||
|
expect(mngr.__list).to.be.empty;
|
||||||
|
// @ts-ignore [allow-private-in-test]
|
||||||
|
expect(mngr.__shownList).to.be.empty;
|
||||||
|
// @ts-ignore [allow-private-in-test]
|
||||||
|
expect(mngr.__siblingsInert).to.be.false;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can add/remove controllers', () => {
|
it('can add/remove controllers', () => {
|
||||||
|
|
|
||||||
|
|
@ -18,25 +18,15 @@ describe('Global Positioning', () => {
|
||||||
overlays.teardown();
|
overlays.teardown();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Basics', () => {
|
|
||||||
it('puts ".contentNode" in the body of the page', async () => {
|
|
||||||
const ctrl = new OverlayController({
|
|
||||||
...withDefaultGlobalConfig(),
|
|
||||||
});
|
|
||||||
await ctrl.show();
|
|
||||||
expect(overlays.globalRootNode.children.length).to.equal(1);
|
|
||||||
expect(overlays.globalRootNode.children[0]).to.have.trimmed.text('my content');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('viewportConfig', () => {
|
describe('viewportConfig', () => {
|
||||||
it('positions the overlay in center by default', async () => {
|
it('positions the overlay in center by default', async () => {
|
||||||
const ctrl = new OverlayController({
|
const ctrl = new OverlayController({
|
||||||
...withDefaultGlobalConfig(),
|
...withDefaultGlobalConfig(),
|
||||||
});
|
});
|
||||||
await ctrl.show();
|
await ctrl.show();
|
||||||
expect(ctrl.content.classList.contains('global-overlays__overlay-container--center')).to.be
|
expect(
|
||||||
.true;
|
ctrl.contentWrapperNode.classList.contains('global-overlays__overlay-container--center'),
|
||||||
|
).to.be.true;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('positions relative to the viewport ', async () => {
|
it('positions relative to the viewport ', async () => {
|
||||||
|
|
@ -60,7 +50,7 @@ describe('Global Positioning', () => {
|
||||||
});
|
});
|
||||||
await ctrl.show();
|
await ctrl.show();
|
||||||
expect(
|
expect(
|
||||||
ctrl.content.classList.contains(
|
ctrl.contentWrapperNode.classList.contains(
|
||||||
`global-overlays__overlay-container--${viewportPlacement}`,
|
`global-overlays__overlay-container--${viewportPlacement}`,
|
||||||
),
|
),
|
||||||
).to.be.true;
|
).to.be.true;
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,39 @@ import { normalizeTransformStyle } from './utils-tests/local-positioning-helpers
|
||||||
* @typedef {import('../types/OverlayConfig.js').ViewportPlacement} ViewportPlacement
|
* @typedef {import('../types/OverlayConfig.js').ViewportPlacement} ViewportPlacement
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make sure we never use a native button element, since its dimensions
|
||||||
|
* are not cross browser consistent
|
||||||
|
* For debugging purposes, add colors...
|
||||||
|
* @param {{clickHandler?: function; width?: number; height?: number}} opts
|
||||||
|
*/
|
||||||
|
function createInvokerSync({ clickHandler = () => {}, width = 100, height = 20 }) {
|
||||||
|
return /** @type {HTMLDivElement} */ (
|
||||||
|
fixtureSync(html`
|
||||||
|
<div
|
||||||
|
role="button"
|
||||||
|
style="width: ${width}px; height: ${height}px; background: red; color: white;"
|
||||||
|
@click=${clickHandler}
|
||||||
|
>
|
||||||
|
Invoker
|
||||||
|
</div>
|
||||||
|
`)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {{ width?: number; height?: number }} opts
|
||||||
|
*/
|
||||||
|
function createContentSync({ width = 80, height = 20 }) {
|
||||||
|
return /** @type {HTMLDivElement} */ (
|
||||||
|
fixtureSync(html`
|
||||||
|
<div style="width: ${width}px; height: ${height}px; background: green; color: white;">
|
||||||
|
Content
|
||||||
|
</div>
|
||||||
|
`)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const withLocalTestConfig = () =>
|
const withLocalTestConfig = () =>
|
||||||
/** @type {OverlayConfig} */ ({
|
/** @type {OverlayConfig} */ ({
|
||||||
placementMode: 'local',
|
placementMode: 'local',
|
||||||
|
|
@ -47,14 +80,13 @@ describe('Local Positioning', () => {
|
||||||
});
|
});
|
||||||
await fixture(html`
|
await fixture(html`
|
||||||
<div style="position: fixed; left: 100px; top: 100px;">
|
<div style="position: fixed; left: 100px; top: 100px;">
|
||||||
${ctrl.invokerNode}${ctrl.content}
|
${ctrl.invokerNode}${ctrl.__wrappingDialogNode}
|
||||||
</div>
|
</div>
|
||||||
`);
|
`);
|
||||||
await ctrl.show();
|
await ctrl.show();
|
||||||
|
|
||||||
expect(normalizeTransformStyle(ctrl.content.style.transform)).to.equal(
|
expect(normalizeTransformStyle(ctrl.contentWrapperNode.style.transform)).to.equal(
|
||||||
'translate(-30px, -18px)',
|
'translate(70px, -508px)',
|
||||||
'translate should be -30px [to center = (80 - 20)/2*-1], -18px [to place above = 10 invoker height + 8 default padding]',
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -76,11 +108,11 @@ describe('Local Positioning', () => {
|
||||||
});
|
});
|
||||||
await fixture(html`
|
await fixture(html`
|
||||||
<div style="position: fixed; left: 100px; top: 100px;">
|
<div style="position: fixed; left: 100px; top: 100px;">
|
||||||
${ctrl.invokerNode}${ctrl.content}
|
${ctrl.invokerNode}${ctrl.contentWrapperNode}
|
||||||
</div>
|
</div>
|
||||||
`);
|
`);
|
||||||
await ctrl.show();
|
await ctrl.show();
|
||||||
expect(ctrl.content.getAttribute('data-popper-placement')).to.equal('top');
|
expect(ctrl.contentWrapperNode.getAttribute('data-popper-placement')).to.equal('top');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('positions to preferred place if placement is set and space is available', async () => {
|
it('positions to preferred place if placement is set and space is available', async () => {
|
||||||
|
|
@ -104,12 +136,12 @@ describe('Local Positioning', () => {
|
||||||
});
|
});
|
||||||
await fixture(html`
|
await fixture(html`
|
||||||
<div style="position: absolute; left: 120px; top: 50px;">
|
<div style="position: absolute; left: 120px; top: 50px;">
|
||||||
${ctrl.invokerNode}${ctrl.content}
|
${ctrl.invokerNode}${ctrl.contentWrapperNode}
|
||||||
</div>
|
</div>
|
||||||
`);
|
`);
|
||||||
|
|
||||||
await ctrl.show();
|
await ctrl.show();
|
||||||
expect(ctrl.content.getAttribute('data-popper-placement')).to.equal('left-start');
|
expect(ctrl.contentWrapperNode.getAttribute('data-popper-placement')).to.equal('left-start');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('positions to different place if placement is set and no space is available', async () => {
|
it('positions to different place if placement is set and no space is available', async () => {
|
||||||
|
|
@ -130,11 +162,13 @@ describe('Local Positioning', () => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
await fixture(html`
|
await fixture(html`
|
||||||
<div style="position: absolute; top: 50px;">${ctrl.invokerNode}${ctrl.content}</div>
|
<div style="position: absolute; top: 50px;">
|
||||||
|
${ctrl.invokerNode}${ctrl.contentWrapperNode}
|
||||||
|
</div>
|
||||||
`);
|
`);
|
||||||
|
|
||||||
await ctrl.show();
|
await ctrl.show();
|
||||||
expect(ctrl.content.getAttribute('data-popper-placement')).to.equal('right');
|
expect(ctrl.contentWrapperNode.getAttribute('data-popper-placement')).to.equal('right');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('allows the user to override default Popper modifiers', async () => {
|
it('allows the user to override default Popper modifiers', async () => {
|
||||||
|
|
@ -164,7 +198,7 @@ describe('Local Positioning', () => {
|
||||||
});
|
});
|
||||||
await fixture(html`
|
await fixture(html`
|
||||||
<div style="position: absolute; left: 100px; top: 50px;">
|
<div style="position: absolute; left: 100px; top: 50px;">
|
||||||
${ctrl.invokerNode}${ctrl.content}
|
${ctrl.invokerNode}${ctrl.contentWrapperNode}
|
||||||
</div>
|
</div>
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
|
@ -175,37 +209,30 @@ describe('Local Positioning', () => {
|
||||||
it('positions the Popper element correctly on show', async () => {
|
it('positions the Popper element correctly on show', async () => {
|
||||||
const ctrl = new OverlayController({
|
const ctrl = new OverlayController({
|
||||||
...withLocalTestConfig(),
|
...withLocalTestConfig(),
|
||||||
contentNode: /** @type {HTMLElement} */ (
|
contentNode: createContentSync({ width: 80, height: 20 }),
|
||||||
fixtureSync(html` <div style="width: 80px; height: 20px;"></div> `)
|
invokerNode: createInvokerSync({ clickHandler: () => ctrl.show(), width: 100, height: 20 }),
|
||||||
),
|
|
||||||
invokerNode: /** @type {HTMLElement} */ (
|
|
||||||
fixtureSync(html`
|
|
||||||
<div
|
|
||||||
role="button"
|
|
||||||
style="width: 100px; height: 20px;"
|
|
||||||
@click=${() => ctrl.show()}
|
|
||||||
></div>
|
|
||||||
`)
|
|
||||||
),
|
|
||||||
popperConfig: {
|
popperConfig: {
|
||||||
placement: 'top',
|
placement: 'top',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
await fixture(html`
|
await fixture(html`
|
||||||
<div style="position: absolute; top: 300px; left: 100px;">
|
<div style="position: absolute; top: 300px; left: 100px;">
|
||||||
${ctrl.invokerNode}${ctrl.content}
|
${ctrl.invokerNode}${ctrl.__wrappingDialogNode}
|
||||||
</div>
|
</div>
|
||||||
`);
|
`);
|
||||||
|
|
||||||
await ctrl.show();
|
await ctrl.show();
|
||||||
expect(normalizeTransformStyle(ctrl.content.style.transform)).to.equal(
|
|
||||||
'translate(10px, -28px)',
|
// N.B. margin between invoker and content = 8px
|
||||||
'Popper positioning values',
|
expect(normalizeTransformStyle(ctrl.contentWrapperNode.style.transform)).to.equal(
|
||||||
|
'translate(110px, -308px)',
|
||||||
|
'110 = (100 + (100-80)/2); -308= 300 + 8',
|
||||||
);
|
);
|
||||||
|
|
||||||
await ctrl.hide();
|
await ctrl.hide();
|
||||||
await ctrl.show();
|
await ctrl.show();
|
||||||
expect(normalizeTransformStyle(ctrl.content.style.transform)).to.equal(
|
expect(normalizeTransformStyle(ctrl.contentWrapperNode.style.transform)).to.equal(
|
||||||
'translate(10px, -28px)',
|
'translate(110px, -308px)',
|
||||||
'Popper positioning values should be identical after hiding and showing',
|
'Popper positioning values should be identical after hiding and showing',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
@ -241,12 +268,12 @@ describe('Local Positioning', () => {
|
||||||
});
|
});
|
||||||
await fixture(html`
|
await fixture(html`
|
||||||
<div style="position: absolute; top: 300px; left: 100px;">
|
<div style="position: absolute; top: 300px; left: 100px;">
|
||||||
${ctrl.invokerNode} ${ctrl.content}
|
${ctrl.invokerNode} ${ctrl.contentWrapperNode}
|
||||||
</div>
|
</div>
|
||||||
`);
|
`);
|
||||||
|
|
||||||
await ctrl.show();
|
await ctrl.show();
|
||||||
expect(normalizeTransformStyle(ctrl.content.style.transform)).to.equal(
|
expect(normalizeTransformStyle(ctrl.contentWrapperNode.style.transform)).to.equal(
|
||||||
'translate3d(10px, -30px, 0px)',
|
'translate3d(10px, -30px, 0px)',
|
||||||
'Popper positioning values',
|
'Popper positioning values',
|
||||||
);
|
);
|
||||||
|
|
@ -267,7 +294,7 @@ describe('Local Positioning', () => {
|
||||||
});
|
});
|
||||||
await ctrl.show();
|
await ctrl.show();
|
||||||
expect(ctrl._popper.options.modifiers.offset.offset).to.equal('0, 20px');
|
expect(ctrl._popper.options.modifiers.offset.offset).to.equal('0, 20px');
|
||||||
expect(normalizeTransformStyle(ctrl.content.style.transform)).to.equal(
|
expect(normalizeTransformStyle(ctrl.contentWrapperNode.style.transform)).to.equal(
|
||||||
'translate3d(10px, -40px, 0px)',
|
'translate3d(10px, -40px, 0px)',
|
||||||
'Popper positioning Y value should be 10 less than previous, due to the added extra 10px offset',
|
'Popper positioning Y value should be 10 less than previous, due to the added extra 10px offset',
|
||||||
);
|
);
|
||||||
|
|
@ -302,12 +329,12 @@ describe('Local Positioning', () => {
|
||||||
});
|
});
|
||||||
await fixture(html`
|
await fixture(html`
|
||||||
<div style="position: absolute; top: 300px; left: 100px;">
|
<div style="position: absolute; top: 300px; left: 100px;">
|
||||||
${ctrl.invokerNode} ${ctrl.content}
|
${ctrl.invokerNode} ${ctrl.contentWrapperNode}
|
||||||
</div>
|
</div>
|
||||||
`);
|
`);
|
||||||
|
|
||||||
await ctrl.show();
|
await ctrl.show();
|
||||||
expect(normalizeTransformStyle(ctrl.content.style.transform)).to.equal(
|
expect(normalizeTransformStyle(ctrl.contentWrapperNode.style.transform)).to.equal(
|
||||||
'translate3d(10px, -30px, 0px)',
|
'translate3d(10px, -30px, 0px)',
|
||||||
'Popper positioning values',
|
'Popper positioning values',
|
||||||
);
|
);
|
||||||
|
|
@ -317,7 +344,7 @@ describe('Local Positioning', () => {
|
||||||
modifiers: [{ name: 'offset', enabled: true, options: { offset: [0, 20] } }],
|
modifiers: [{ name: 'offset', enabled: true, options: { offset: [0, 20] } }],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(normalizeTransformStyle(ctrl.content.style.transform)).to.equal(
|
expect(normalizeTransformStyle(ctrl.contentWrapperNode.style.transform)).to.equal(
|
||||||
'translate3d(10px, -40px, 0px)',
|
'translate3d(10px, -40px, 0px)',
|
||||||
'Popper positioning Y value should be 10 less than previous, due to the added extra 10px offset',
|
'Popper positioning Y value should be 10 less than previous, due to the added extra 10px offset',
|
||||||
);
|
);
|
||||||
|
|
@ -333,7 +360,7 @@ describe('Local Positioning', () => {
|
||||||
invokerNode,
|
invokerNode,
|
||||||
});
|
});
|
||||||
await ctrl.show();
|
await ctrl.show();
|
||||||
expect(ctrl.content.style.minWidth).to.equal('60px');
|
expect(ctrl.contentWrapperNode.style.minWidth).to.equal('60px');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can set the contentNode maxWidth as the invokerNode width', async () => {
|
it('can set the contentNode maxWidth as the invokerNode width', async () => {
|
||||||
|
|
@ -346,7 +373,7 @@ describe('Local Positioning', () => {
|
||||||
invokerNode,
|
invokerNode,
|
||||||
});
|
});
|
||||||
await ctrl.show();
|
await ctrl.show();
|
||||||
expect(ctrl.content.style.maxWidth).to.equal('60px');
|
expect(ctrl.contentWrapperNode.style.maxWidth).to.equal('60px');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can set the contentNode width as the invokerNode width', async () => {
|
it('can set the contentNode width as the invokerNode width', async () => {
|
||||||
|
|
@ -359,7 +386,7 @@ describe('Local Positioning', () => {
|
||||||
invokerNode,
|
invokerNode,
|
||||||
});
|
});
|
||||||
await ctrl.show();
|
await ctrl.show();
|
||||||
expect(ctrl.content.style.width).to.equal('60px');
|
expect(ctrl.contentWrapperNode.style.width).to.equal('60px');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ export interface OverlayConfig {
|
||||||
elementToFocusAfterHide?: HTMLElement;
|
elementToFocusAfterHide?: HTMLElement;
|
||||||
/** Whether it should have a backdrop (currently exclusive to globalOverlayController) */
|
/** Whether it should have a backdrop (currently exclusive to globalOverlayController) */
|
||||||
hasBackdrop?: boolean;
|
hasBackdrop?: boolean;
|
||||||
/** Hides other overlays when mutiple are opened (currently exclusive to globalOverlayController) */
|
/** Hides other overlays when multiple are opened (currently exclusive to globalOverlayController) */
|
||||||
isBlocking?: boolean;
|
isBlocking?: boolean;
|
||||||
/** Prevents scrolling body content when overlay opened (currently exclusive to globalOverlayController) */
|
/** Prevents scrolling body content when overlay opened (currently exclusive to globalOverlayController) */
|
||||||
preventsScroll?: boolean;
|
preventsScroll?: boolean;
|
||||||
|
|
@ -50,6 +50,12 @@ export interface OverlayConfig {
|
||||||
popperConfig?: Partial<Options>;
|
popperConfig?: Partial<Options>;
|
||||||
/** Viewport configuration. Will be used when placementMode is 'global' */
|
/** Viewport configuration. Will be used when placementMode is 'global' */
|
||||||
viewportConfig?: ViewportConfig;
|
viewportConfig?: ViewportConfig;
|
||||||
|
|
||||||
|
/** Change the default of 9999 */
|
||||||
|
zIndex?: number;
|
||||||
|
|
||||||
|
/** render a div instead of dialog */
|
||||||
|
_noDialogEl?: Boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ViewportPlacement =
|
export type ViewportPlacement =
|
||||||
|
|
@ -61,8 +67,7 @@ export type ViewportPlacement =
|
||||||
| 'bottom-right'
|
| 'bottom-right'
|
||||||
| 'bottom'
|
| 'bottom'
|
||||||
| 'bottom-left'
|
| 'bottom-left'
|
||||||
| 'left'
|
| 'left';
|
||||||
| 'center';
|
|
||||||
|
|
||||||
export interface ViewportConfig {
|
export interface ViewportConfig {
|
||||||
placement: ViewportPlacement;
|
placement: ViewportPlacement;
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,8 @@ export interface DefineOverlayConfig {
|
||||||
|
|
||||||
export declare class OverlayHost {
|
export declare class OverlayHost {
|
||||||
opened: Boolean;
|
opened: Boolean;
|
||||||
get config(): OverlayConfig;
|
get config(): Partial<OverlayConfig>;
|
||||||
set config(value: OverlayConfig);
|
set config(value: Partial<OverlayConfig>);
|
||||||
|
|
||||||
open(): Promise<void>;
|
open(): Promise<void>;
|
||||||
close(): Promise<void>;
|
close(): Promise<void>;
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,7 @@ import { SlotMixin } from '@lion/ui/core.js';
|
||||||
export class LionSelectInvoker extends SlotMixin(LionButton) {
|
export class LionSelectInvoker extends SlotMixin(LionButton) {
|
||||||
static get styles() {
|
static get styles() {
|
||||||
return [
|
return [
|
||||||
// TODO switch back to ...super.styles once fixed https://github.com/lit/lit.dev/pull/535
|
...super.styles,
|
||||||
...LionButton.styles,
|
|
||||||
css`
|
css`
|
||||||
:host {
|
:host {
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|
@ -31,9 +30,9 @@ export class LionSelectInvoker extends SlotMixin(LionButton) {
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @type {any} */
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
...super.properties,
|
|
||||||
selectedElement: { type: Object },
|
selectedElement: { type: Object },
|
||||||
hostElement: { type: Object },
|
hostElement: { type: Object },
|
||||||
readOnly: { type: Boolean, reflect: true, attribute: 'readonly' },
|
readOnly: { type: Boolean, reflect: true, attribute: 'readonly' },
|
||||||
|
|
|
||||||
|
|
@ -408,15 +408,18 @@ export class LionSelectRich extends SlotMixin(ScopedElementsMixin(OverlayMixin(L
|
||||||
* @protected
|
* @protected
|
||||||
*/
|
*/
|
||||||
async _alignInvokerWidth() {
|
async _alignInvokerWidth() {
|
||||||
if (this._overlayCtrl && this._overlayCtrl.content) {
|
if (!this._overlayCtrl?.content) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await this.updateComplete;
|
await this.updateComplete;
|
||||||
const initContentDisplay = this._overlayCtrl.content.style.display;
|
const initContentDisplay = this._overlayCtrl.content.style.display;
|
||||||
const initContentMinWidth = this._overlayCtrl.content.style.minWidth;
|
const initContentMinWidth = this._overlayCtrl.contentWrapperNode.style.minWidth;
|
||||||
const initContentWidth = this._overlayCtrl.content.style.width;
|
const initContentWidth = this._overlayCtrl.contentWrapperNode.style.width;
|
||||||
this._overlayCtrl.content.style.display = '';
|
this._overlayCtrl.content.style.display = '';
|
||||||
this._overlayCtrl.content.style.minWidth = 'auto';
|
this._overlayCtrl.contentWrapperNode.style.minWidth = 'auto';
|
||||||
this._overlayCtrl.content.style.width = 'auto';
|
this._overlayCtrl.contentWrapperNode.style.width = 'auto';
|
||||||
const contentWidth = this._overlayCtrl.content.getBoundingClientRect().width;
|
const contentWidth = this._overlayCtrl.contentWrapperNode.getBoundingClientRect().width;
|
||||||
/**
|
/**
|
||||||
* TODO when inside an overlay the current solution doesn't work.
|
* TODO when inside an overlay the current solution doesn't work.
|
||||||
* Since that dialog is still hidden, open and close the select-rich
|
* Since that dialog is still hidden, open and close the select-rich
|
||||||
|
|
@ -426,9 +429,8 @@ export class LionSelectRich extends SlotMixin(ScopedElementsMixin(OverlayMixin(L
|
||||||
this._invokerNode.style.width = `${contentWidth + this._arrowWidth}px`;
|
this._invokerNode.style.width = `${contentWidth + this._arrowWidth}px`;
|
||||||
}
|
}
|
||||||
this._overlayCtrl.content.style.display = initContentDisplay;
|
this._overlayCtrl.content.style.display = initContentDisplay;
|
||||||
this._overlayCtrl.content.style.minWidth = initContentMinWidth;
|
this._overlayCtrl.contentWrapperNode.style.minWidth = initContentMinWidth;
|
||||||
this._overlayCtrl.content.style.width = initContentWidth;
|
this._overlayCtrl.contentWrapperNode.style.width = initContentWidth;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { LitElement } from 'lit';
|
import { LitElement } from 'lit';
|
||||||
import { renderLitAsNode } from '@lion/ui/helpers.js';
|
|
||||||
import { LionOption } from '@lion/ui/listbox.js';
|
import { LionOption } from '@lion/ui/listbox.js';
|
||||||
import { getListboxMembers } from '@lion/ui/listbox-test-helpers.js';
|
import { getListboxMembers } from '@lion/ui/listbox-test-helpers.js';
|
||||||
import { OverlayController } from '@lion/ui/overlays.js';
|
import { OverlayController } from '@lion/ui/overlays.js';
|
||||||
|
|
@ -152,7 +151,7 @@ describe('lion-select-rich', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('updates the invoker when the selected element is the same but the modelValue was updated asynchronously', async () => {
|
it('updates the invoker when the selected element is the same but the modelValue was updated asynchronously', async () => {
|
||||||
const tag = defineCE(
|
const tagString = defineCE(
|
||||||
class LionCustomOption extends LionOption {
|
class LionCustomOption extends LionOption {
|
||||||
render() {
|
render() {
|
||||||
return html`${this.modelValue.value}`;
|
return html`${this.modelValue.value}`;
|
||||||
|
|
@ -163,27 +162,26 @@ describe('lion-select-rich', () => {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const tagString = unsafeStatic(tag);
|
const tag = unsafeStatic(tagString);
|
||||||
|
|
||||||
const firstOption = /** @type {LionOption} */ (
|
const firstOption = /** @type {LionOption} */ (
|
||||||
renderLitAsNode(html`<${tagString} checked .choiceValue=${10}></${tagString}>`)
|
await _fixture(html`<${tag} checked .choiceValue=${10}></${tag}>`)
|
||||||
);
|
);
|
||||||
|
|
||||||
const el = await fixture(html`
|
const el = await fixture(html`
|
||||||
<lion-select-rich>
|
<lion-select-rich>
|
||||||
${firstOption}
|
${firstOption}
|
||||||
<${tagString} .choiceValue=${20}></${tagString}>
|
<${tag} .choiceValue=${20}></${tag}>
|
||||||
</lion-select-rich>
|
</lion-select-rich>
|
||||||
`);
|
`);
|
||||||
|
|
||||||
const { _invokerNode } = getSelectRichMembers(el);
|
const { _invokerNode } = getSelectRichMembers(el);
|
||||||
|
|
||||||
const firstChild = /** @type {HTMLElement} */ (
|
const firstChild = /** @type {HTMLElement} */ (
|
||||||
/** @type {ShadowRoot} */ (_invokerNode.shadowRoot).firstElementChild
|
/** @type {ShadowRoot} */ (_invokerNode.shadowRoot).firstElementChild
|
||||||
);
|
);
|
||||||
expect(firstChild.textContent).to.equal('10');
|
expect(firstChild.textContent).to.equal('10');
|
||||||
|
|
||||||
firstOption.modelValue = { value: 30, checked: true };
|
firstOption.modelValue = { value: 30, checked: true };
|
||||||
await firstOption.updateComplete;
|
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
expect(firstChild.textContent).to.equal('30');
|
expect(firstChild.textContent).to.equal('30');
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,18 @@
|
||||||
import { css, LitElement } from 'lit';
|
import { css, LitElement } from 'lit';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {import('../types/OverlayConfig').OverlayConfig} OverlayConfig
|
* @typedef {import('@lion/ui/types/overlays.js').OverlayConfig} OverlayConfig
|
||||||
*/
|
*/
|
||||||
class DemoOverlayBackdrop extends LitElement {
|
class DemoOverlayBackdrop extends LitElement {
|
||||||
static get styles() {
|
static get styles() {
|
||||||
return css`
|
return css`
|
||||||
:host {
|
:host {
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
z-index: 1;
|
|
||||||
background-color: grey;
|
background-color: grey;
|
||||||
opacity: 0.3;
|
opacity: 0.3;
|
||||||
display: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:host(.local-overlays__backdrop--visible) {
|
:host(.local-overlays__backdrop--visible) {
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { html, LitElement } from 'lit';
|
||||||
import { OverlayMixin } from '@lion/ui/overlays.js';
|
import { OverlayMixin } from '@lion/ui/overlays.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {import('../types/OverlayConfig').OverlayConfig} OverlayConfig
|
* @typedef {import('../types/OverlayConfig.js').OverlayConfig} OverlayConfig
|
||||||
*/
|
*/
|
||||||
class DemoOverlaySystem extends OverlayMixin(LitElement) {
|
class DemoOverlaySystem extends OverlayMixin(LitElement) {
|
||||||
// eslint-disable-next-line class-methods-use-this
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
export { OverlayConfig } from '../../components/overlays/types/OverlayConfig.js';
|
export { OverlayConfig } from '../../components/overlays/types/OverlayConfig.js';
|
||||||
|
export { ViewportConfig } from '../../components/overlays/types/OverlayConfig.js';
|
||||||
export { DefineOverlayConfig } from '../../components/overlays/types/OverlayMixinTypes.js';
|
export { DefineOverlayConfig } from '../../components/overlays/types/OverlayMixinTypes.js';
|
||||||
export { OverlayHost } from '../../components/overlays/types/OverlayMixinTypes.js';
|
export { OverlayHost } from '../../components/overlays/types/OverlayMixinTypes.js';
|
||||||
export { ArrowHost } from '../../components/overlays/types/ArrowMixinTypes.js';
|
export { ArrowHost } from '../../components/overlays/types/ArrowMixinTypes.js';
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue