fix: demos and overlay styles

chore: overlay documentation fixes
This commit is contained in:
Thijs Louisse 2022-12-06 23:17:55 +01:00 committed by Thijs Louisse
parent 9d31c179b1
commit 0bc604d600
31 changed files with 565 additions and 542 deletions

View file

@ -17,10 +17,10 @@ export const main = () => html`
</style> </style>
<lion-dialog> <lion-dialog>
<button slot="invoker">Click me to open dialog</button> <button slot="invoker">Click me to open dialog</button>
<div slot="content" class="dialog"> <div slot="content" class="demo-dialog-content">
Hello! You can close this dialog here: Hello! You can close this dialog here:
<button <button
class="close-button" class="demo-dialog-content__close-button"
@click=${e => e.target.dispatchEvent(new Event('close-overlay', { bubbles: true }))} @click=${e => e.target.dispatchEvent(new Event('close-overlay', { bubbles: true }))}
> >
@ -48,16 +48,12 @@ import { LionDialog } from '@lion/ui/dialog.js';
import '@lion/ui/define/lion-dialog.js'; import '@lion/ui/define/lion-dialog.js';
``` ```
- Your `slot="content"` node will be moved to the global overlay container during initialization.
After, your content node is no longer a child of `lion-dialog`.
If you still need to access it from the `lion-dialog` you can do so by using the `._overlayContentNode` property.
- To close the overlay from within the content node, you need to dispatch a `close-overlay` event that bubbles. - To close the overlay from within the content node, you need to dispatch a `close-overlay` event that bubbles.
It has to be able to reach the content node. It has to be able to reach the content node (if you need to traverse shadow boundaries, you will have to add `composed: true` as well).
- If you need to traverse shadow boundaries, you will have to add `composed: true` as well, although this is discouraged as a practice.
## Changing the configuration ## Changing the configuration
You can use the `config` property on the dialog to change the configuration. You can use the `.config` property on the dialog to change the configuration.
The documentation of the full config object can be found in the `lion/overlay` package or here in [Overlay System - Configuration](../../fundamentals/systems/overlays/configuration.md). The documentation of the full config object can be found in the `overlays` folder or here in [Overlay System - Configuration](../../fundamentals/systems/overlays/configuration.md).
The `config` property uses a setter to merge the passed configuration with the current, so you only **overwrite what you pass** when updating `config`. The `config` property uses a setter to merge the passed configuration with the current, so you only **overwrite what you pass** when updating `config`.

View file

@ -1,3 +0,0 @@
import { css } from 'lit';
export default css``;

View file

@ -36,4 +36,22 @@ export const demoStyle = css`
border-radius: 4px; border-radius: 4px;
padding: 8px; padding: 8px;
} }
.demo-dialog-content {
display: block;
position: absolute;
font-size: 16px;
color: white;
background-color: black;
border-radius: 4px;
padding: 8px;
}
.demo-dialog-content__close-button {
color: black;
font-size: 28px;
line-height: 28px;
padding: 0;
border-style: none;
}
`; `;

View file

@ -22,73 +22,27 @@ import './src/slots-dialog-content.js';
</lion-dialog> </lion-dialog>
``` ```
## Styling content
It's not possible to style content from the dialog component. This is because the content slot is moved to the global root node. This is why a custom component should be created and slotted in as the content. This ensures style encapsulation on the dialog content.
```js preview-story
export const stylingContent = () => html`
<style>
${demoStyle}
</style>
<lion-dialog .config=${{ hidesOnOutsideClick: true, hidesOnEsc: true }}>
<button slot="invoker">Styled dialog</button>
<styled-dialog-content slot="content"></styled-dialog-content>
</lion-dialog>
`;
```
## Content with slots
```js preview-story
export const slotsContent = () => html`
<style>
${demoStyle}
</style>
<lion-dialog .config=${{ hidesOnOutsideClick: true, hidesOnEsc: true }}>
<button slot="invoker">Dialog with content with slots</button>
<slots-dialog-content slot="content">
<p>Some Stuff</p>
<p slot="actions">I am in the actions slot</p>
</slots-dialog-content>
</lion-dialog>
`;
```
## Close overlay from component slotted as content
The overlay cannot be closed by dispatching the `close-overlay` from a button in a styled component that is slotted in as content, because it will not cross the shadow boundary of the component. A method should be created that will dispatch the `close-overlay` event from the component.
```js preview-story
export const closeOverlayFromComponent = () => html`
<style>
${demoStyle}
</style>
<lion-dialog .config=${{ hidesOnOutsideClick: true, hidesOnEsc: true }}>
<button slot="invoker">Styled dialog</button>
<styled-dialog-content slot="content"></styled-dialog-content>
</lion-dialog>
`;
```
## Placement overrides ## Placement overrides
```js preview-story ```js preview-story
export const placementOverrides = () => { export const placementOverrides = () => {
const dialog = placement => html` const dialog = placement => {
<lion-dialog .config="${{ viewportConfig: { placement } }}"> const cfg = { viewportConfig: { placement } };
<button slot="invoker">Dialog ${placement}</button> return html`
<div slot="content" class="dialog demo-box"> <lion-dialog .config="${cfg}">
Hello! You can close this notification here: <button slot="invoker">Dialog ${placement}</button>
<button <div slot="content" class="dialog demo-box">
class="close-button" Hello! You can close this notification here:
@click=${e => e.target.dispatchEvent(new Event('close-overlay', { bubbles: true }))} <button
> class="close-button"
@click=${e => e.target.dispatchEvent(new Event('close-overlay', { bubbles: true }))}
</button> >
</div>
</lion-dialog> </button>
`; </div>
</lion-dialog>
`;
};
return html` return html`
<style> <style>
${demoStyle} ${demoStyle}
@ -116,30 +70,32 @@ Configuration passed to `config` property:
No backdrop, hides on escape, prevents scrolling while opened, and focuses the body when hiding. No backdrop, hides on escape, prevents scrolling while opened, and focuses the body when hiding.
```js preview-story ```js preview-story
export const otherOverrides = () => html` export const otherOverrides = () => {
<style> const cfg = {
${demoStyle} hasBackdrop: false,
</style> hidesOnEscape: true,
<lion-dialog preventsScroll: true,
.config=${{ elementToFocusAfterHide: document.body,
hasBackdrop: false, };
hidesOnEscape: true,
preventsScroll: true, return html`
elementToFocusAfterHide: document.body, <style>
}} ${demoStyle}
> </style>
<button slot="invoker">Click me to open dialog</button> <lion-dialog .config="${cfg}">
<div slot="content" class="dialog"> <button slot="invoker">Click me to open dialog</button>
Hello! You can close this dialog here: <div slot="content" class="demo-dialog-content">
<button Hello! You can close this dialog here:
class="close-button" <button
@click=${e => e.target.dispatchEvent(new Event('close-overlay', { bubbles: true }))} class="demo-dialog-content__close-button"
> @click=${e => e.target.dispatchEvent(new Event('close-overlay', { bubbles: true }))}
>
</button>
</div> </button>
</lion-dialog> </div>
`; </lion-dialog>
`;
};
``` ```
Configuration passed to `config` property: Configuration passed to `config` property:

View file

@ -0,0 +1,116 @@
---
eleventyExcludeFromCollections: true
---
# Systems >> Overlays >> Configuration >> Positioning ||40
```js script
import { html, render, LitElement } from '@mdjs/mdjs-preview';
import { ref, createRef } from 'lit/directives/ref.js';
import './assets/demo-el-using-overlaymixin.mjs';
import './assets/applyDemoOverlayStyles.mjs';
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>`;
```
## placementMode
The `placementMode` property determines the positioning of the `contentNode`:
- next to its reference node: `local`
- relative to the viewport: `global`
### Local
<!-- By default, the [`referenceNode`](./configuration-elements#referencenode) is the [invokerNode](/configuration-elements#invokernode). -->
```js story
export const placementLocal = () => {
const placementModeLocalConfig = { placementMode: 'local' };
return html`
<demo-el-using-overlaymixin .config=${placementModeLocalConfig}>
<button slot="invoker">Click me to open the local overlay!</button>
<div slot="content" class="demo-overlay">
Hello! You can close this notification here:
<button
class="close-button"
@click=${e => e.target.dispatchEvent(new Event('close-overlay', { bubbles: true }))}
>
</button>
</div>
</demo-el-using-overlaymixin>
`;
};
```
### Global
```js story
export const placementGlobal = () => {
const placementModeGlobalConfig = { placementMode: 'global' };
return html`
<demo-el-using-overlaymixin .config=${placementModeGlobalConfig}>
<button slot="invoker">Click me to open the global overlay!</button>
<div slot="content" class="demo-overlay">
Hello! You can close this notification here:
<button
class="close-button"
@click=${e => e.target.dispatchEvent(new Event('close-overlay', { bubbles: true }))}
>
</button>
</div>
</demo-el-using-overlaymixin>
`;
};
```
## popperConfig
/** Viewport configuration. Will be used when placementMode is 'global' \*/
viewportConfig?: ViewportConfig;
/** Hides other overlays when multiple are opened (currently exclusive to globalOverlayController) _/
isBlocking?: boolean;
/\*\* Will align contentNode with referenceNode (invokerNode by default) for local overlays. Usually needed for dropdowns. 'max' will prevent contentNode from exceeding width of referenceNode, 'min' guarantees that contentNode will be at least as wide as referenceNode. 'full' will make sure that the invoker width always is the same. _/
inheritsReferenceWidth?: 'max' | 'full' | 'min' | 'none';
/\*_ Change the default of 9999 _/
zIndex?: number;
| Prop | Description | Type | | | |
| ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- | --- | --- | --- |
| placementMode | Determines the positioning anchor (viewport vs invokerNode/referenceNode) | 'global'\|'local' | | | |
| viewportConfig | Viewport positioning configuration. Will be used when placementMode is 'global' | {placement: ViewportPlacement} | | | |
| popperConfig | Anchor positioning configuration. Will be used when placementMode is 'local' | | | | |
| inheritsReferenceWidth | Will align contentNode with referenceNode for local overlays. Usually needed for dropdowns. 'max' will prevent contentNode from exceeding width of referenceNode, 'min' guarantees that contentNode will be at least as wide as referenceNode. 'full' will make sure that the invoker width always is the same. | 'max' \| 'full' \| 'min' \| 'none' | | | |

View file

@ -1,43 +1,17 @@
# Systems >> Overlays >> Positioning ||10 ---
eleventyExcludeFromCollections: true
---
# Systems >> Overlays >> Edge cases ||10
```js script ```js script
import { html, render, LitElement } from '@mdjs/mdjs-preview'; import { html, render, LitElement } from '@mdjs/mdjs-preview';
import { ref, createRef } from 'lit/directives/ref.js'; import { ref, createRef } from 'lit/directives/ref.js';
import './assets/demo-el-using-overlaymixin.mjs';
import './assets/applyDemoOverlayStyles.mjs';
import './assets/demo-overlay-positioning.mjs'; 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 ## 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. 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.

View file

@ -15,7 +15,7 @@ const applyDemoOverlayStyles = () => {
`; `;
const styleTag = document.createElement('style'); const styleTag = document.createElement('style');
styleTag.setAttribute('data-demo-global-overlays', ''); styleTag.setAttribute('data-demo-overlays', '');
styleTag.textContent = demoOverlaysStyle.cssText; styleTag.textContent = demoOverlaysStyle.cssText;
document.head.appendChild(styleTag); document.head.appendChild(styleTag);
}; };

View file

@ -7,7 +7,7 @@ import { LionButton } from '@lion/ui/button.js';
/** /**
* @typedef {import('@lion/ui/types/overlays.js').OverlayConfig} OverlayConfig * @typedef {import('@lion/ui/types/overlays.js').OverlayConfig} OverlayConfig
*/ */
class DemoOverlaySystem extends OverlayMixin(LitElement) { class DemoElUsingOverlayMixin extends OverlayMixin(LitElement) {
// eslint-disable-next-line class-methods-use-this // eslint-disable-next-line class-methods-use-this
_defineOverlayConfig() { _defineOverlayConfig() {
return /** @type {OverlayConfig} */ ({ return /** @type {OverlayConfig} */ ({
@ -36,12 +36,10 @@ class DemoOverlaySystem extends OverlayMixin(LitElement) {
<slot name="invoker"></slot> <slot name="invoker"></slot>
<slot name="backdrop"></slot> <slot name="backdrop"></slot>
<slot name="content"></slot> <slot name="content"></slot>
<div>popup is ${this.opened ? 'opened' : 'closed'}</div>
`; `;
} }
} }
customElements.define('demo-overlay-system', DemoOverlaySystem); customElements.define('demo-el-using-overlaymixin', DemoElUsingOverlayMixin);
class DemoOverlay extends OverlayMixin(LitElement) { class DemoOverlay extends OverlayMixin(LitElement) {
static get styles() { static get styles() {

View file

@ -14,6 +14,7 @@ class DemoOverlayBackdrop extends LitElement {
height: 100%; height: 100%;
background-color: grey; background-color: grey;
opacity: 0.3; opacity: 0.3;
position: fixed;
} }
:host(.local-overlays__backdrop--visible) { :host(.local-overlays__backdrop--visible) {

View file

@ -3,7 +3,7 @@
import { html, LitElement, css } from 'lit'; import { html, LitElement, css } from 'lit';
import { ref, createRef } from 'lit/directives/ref.js'; import { ref, createRef } from 'lit/directives/ref.js';
import { OverlayMixin } from '@lion/ui/overlays.js'; import { OverlayMixin } from '@lion/ui/overlays.js';
import './demo-overlay-system.mjs'; import './demo-el-using-overlaymixin.mjs';
/** /**
* @typedef {import('@lion/ui/types/overlays.js').OverlayConfig} OverlayConfig * @typedef {import('@lion/ui/types/overlays.js').OverlayConfig} OverlayConfig
@ -44,8 +44,7 @@ class DemoOverlayEl extends OverlayMixin(LitElement) {
height: 100%; height: 100%;
} }
:host([simulate-viewport]) :host([simulate-viewport]) #overlay-content-node-wrapper.overlays__overlay-container {
#overlay-content-node-wrapper.global-overlays__overlay-container {
position: absolute; position: absolute;
} }

View file

@ -2,7 +2,7 @@
```js script ```js script
import { html } from '@mdjs/mdjs-preview'; import { html } from '@mdjs/mdjs-preview';
import './assets/demo-overlay-system.mjs'; import './assets/demo-el-using-overlaymixin.mjs';
import './assets/applyDemoOverlayStyles.mjs'; import './assets/applyDemoOverlayStyles.mjs';
``` ```
@ -14,7 +14,7 @@ The `OverlayMixin` exposes these options via `.config`.
Either `'local'` or `'global'`. Either `'local'` or `'global'`.
This determines the DOM position of the `contentNode`, either next to the invokerNode, This determines the DOM position of the `contentNode`, either next to the invokerNode,
or in the `global-overlays` container at the bottom of the `<body>`. or in the `overlays` container at the bottom of the `<body>`.
### Local ### Local
@ -28,7 +28,7 @@ export const placementLocal = () => {
border: 1px solid black; border: 1px solid black;
} }
</style> </style>
<demo-overlay-system .config=${placementModeLocalConfig}> <demo-el-using-overlaymixin .config=${placementModeLocalConfig}>
<button slot="invoker">Click me to open the local overlay!</button> <button slot="invoker">Click me to open the local 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:
@ -39,7 +39,7 @@ export const placementLocal = () => {
</button> </button>
</div> </div>
</demo-overlay-system> </demo-el-using-overlaymixin>
`; `;
}; };
``` ```
@ -50,7 +50,7 @@ export const placementLocal = () => {
export const placementGlobal = () => { export const placementGlobal = () => {
const placementModeGlobalConfig = { placementMode: 'global' }; const placementModeGlobalConfig = { placementMode: 'global' };
return html` return html`
<demo-overlay-system .config=${placementModeGlobalConfig}> <demo-el-using-overlaymixin .config=${placementModeGlobalConfig}>
<button slot="invoker">Click me to open the global overlay!</button> <button slot="invoker">Click me to open the global 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:
@ -61,7 +61,7 @@ export const placementGlobal = () => {
</button> </button>
</div> </div>
</demo-overlay-system> </demo-el-using-overlaymixin>
`; `;
}; };
``` ```
@ -91,12 +91,12 @@ export const isTooltip = () => {
const tooltipConfig = { placementMode: 'local', isTooltip: true, handlesAccessibility: true }; const tooltipConfig = { placementMode: 'local', isTooltip: true, handlesAccessibility: true };
return html` return html`
<demo-overlay-system id="tooltip" .config=${tooltipConfig}> <demo-el-using-overlaymixin id="tooltip" .config=${tooltipConfig}>
<button slot="invoker" @mouseenter=${showTooltip} @mouseleave=${hideTooltip}> <button slot="invoker" @mouseenter=${showTooltip} @mouseleave=${hideTooltip}>
Hover me to open the tooltip! Hover me to open the tooltip!
</button> </button>
<div slot="content" class="demo-overlay">Hello!</div> <div slot="content" class="demo-overlay">Hello!</div>
</demo-overlay-system> </demo-el-using-overlaymixin>
`; `;
}; };
``` ```
@ -114,7 +114,7 @@ You use the feature on any type of overlay.
export const trapsKeyboardFocus = () => { export const trapsKeyboardFocus = () => {
const trapsKeyboardFocusConfig = { trapsKeyboardFocus: true }; const trapsKeyboardFocusConfig = { trapsKeyboardFocus: true };
return html` return html`
<demo-overlay-system .config=${trapsKeyboardFocusConfig}> <demo-el-using-overlaymixin .config=${trapsKeyboardFocusConfig}>
<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">
<div><a href="#">A focusable anchor</a></div> <div><a href="#">A focusable anchor</a></div>
@ -127,7 +127,7 @@ export const trapsKeyboardFocus = () => {
</button> </button>
</div> </div>
</demo-overlay-system> </demo-el-using-overlaymixin>
`; `;
}; };
``` ```
@ -140,7 +140,7 @@ Boolean property. Will allow closing the overlay on ESC key when enabled.
export const hidesOnEsc = () => { export const hidesOnEsc = () => {
const hidesOnEscConfig = { hidesOnEsc: true }; const hidesOnEscConfig = { hidesOnEsc: true };
return html` return html`
<demo-overlay-system .config=${hidesOnEscConfig}> <demo-el-using-overlaymixin .config=${hidesOnEscConfig}>
<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:
@ -151,7 +151,7 @@ export const hidesOnEsc = () => {
</button> </button>
</div> </div>
</demo-overlay-system> </demo-el-using-overlaymixin>
`; `;
}; };
``` ```
@ -164,7 +164,7 @@ Boolean property. When enabled allows closing the overlay on ESC key, even when
export const hidesOnOutsideEsc = () => { export const hidesOnOutsideEsc = () => {
const hidesOnEscConfig = { hidesOnOutsideEsc: true }; const hidesOnEscConfig = { hidesOnOutsideEsc: true };
return html` return html`
<demo-overlay-system .config=${hidesOnEscConfig}> <demo-el-using-overlaymixin .config=${hidesOnEscConfig}>
<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:
@ -175,7 +175,7 @@ export const hidesOnOutsideEsc = () => {
</button> </button>
</div> </div>
</demo-overlay-system> </demo-el-using-overlaymixin>
`; `;
}; };
``` ```
@ -188,7 +188,7 @@ Boolean property. Will allow closing the overlay by clicking outside the `conten
export const hidesOnOutsideClick = () => { export const hidesOnOutsideClick = () => {
const hidesOnOutsideClickConfig = { hidesOnOutsideClick: true }; const hidesOnOutsideClickConfig = { hidesOnOutsideClick: true };
return html` return html`
<demo-overlay-system .config=${hidesOnOutsideClickConfig}> <demo-el-using-overlaymixin .config=${hidesOnOutsideClickConfig}>
<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">
<label for="myInput">Clicking this label should not trigger close</label> <label for="myInput">Clicking this label should not trigger close</label>
@ -200,7 +200,7 @@ export const hidesOnOutsideClick = () => {
</button> </button>
</div> </div>
</demo-overlay-system> </demo-el-using-overlaymixin>
`; `;
}; };
``` ```
@ -213,9 +213,12 @@ In the example, we focus the body instead of the `invokerNode`.
```js preview-story ```js preview-story
export const elementToFocusAfterHide = () => { export const elementToFocusAfterHide = () => {
const elementToFocusAfterHideConfig = { elementToFocusAfterHide: document.body }; const btn = document.createElement('button');
btn.innerText = 'I should get focus';
const elementToFocusAfterHideConfig = { elementToFocusAfterHide: btn };
return html` return html`
<demo-overlay-system .config=${elementToFocusAfterHideConfig}> <demo-el-using-overlaymixin .config=${elementToFocusAfterHideConfig}>
<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:
@ -226,7 +229,8 @@ export const elementToFocusAfterHide = () => {
</button> </button>
</div> </div>
</demo-overlay-system> </demo-el-using-overlaymixin>
${btn}
`; `;
}; };
``` ```
@ -239,18 +243,18 @@ Boolean property. When true, will add a backdrop when the overlay is opened.
> If this is not what you intend, you can make the overlays not nested, where opening one, closes the other. > If this is not what you intend, you can make the overlays not nested, where opening one, closes the other.
> Fortunately, we also have a configuration option that simulates that behavior in the next section `isBlocking`. > Fortunately, we also have a configuration option that simulates that behavior in the next section `isBlocking`.
The backdrop styling can be configured by targeting the `.global-overlays .global-overlays__backdrop` css selector. The backdrop styling can be configured by targeting the `.overlays .overlays__backdrop` css selector.
The backdrop animation can be configured by targeting the The backdrop animation can be configured by targeting the
`.global-overlays .global-overlays__backdrop--animation-in` and `.overlays .overlays__backdrop--animation-in` and
`.global-overlays .global-overlays__backdrop--animation-out` css selector. `.overlays .overlays__backdrop--animation-out` css selector.
This currently only supports CSS Animations, because it relies on the `animationend` event to add/remove classes. This currently only supports CSS Animations, because it relies on the `animationend` event to add/remove classes.
```js preview-story ```js preview-story
export const hasBackdrop = () => { export const hasBackdrop = () => {
const hasBackdropConfig = { hasBackdrop: true }; const hasBackdropConfig = { hasBackdrop: true };
return html` return html`
<demo-overlay-system .config=${hasBackdropConfig}> <demo-el-using-overlaymixin .config=${hasBackdropConfig}>
<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:
@ -261,7 +265,7 @@ export const hasBackdrop = () => {
</button> </button>
</div> </div>
</demo-overlay-system> </demo-el-using-overlaymixin>
`; `;
}; };
``` ```
@ -274,24 +278,10 @@ Boolean property. When true, will block other overlays.
export const isBlocking = () => { export const isBlocking = () => {
const isBlockingConfig = { hasBackdrop: true, isBlocking: true }; const isBlockingConfig = { hasBackdrop: true, isBlocking: true };
return html` return html`
<demo-overlay-system> <demo-el-using-overlaymixin>
<button slot="invoker">Click me to open the overlay!</button> <button slot="invoker">Overlay A: open first</button>
<div slot="content" class="demo-overlay"> <div slot="content" class="demo-overlay" style="width:200px;">
<div> This overlay gets closed when overlay B gets opened
<demo-overlay-system .config=${isBlockingConfig}>
<button slot="invoker">Click me to open another overlay which is blocking</button>
<div slot="content" class="demo-overlay demo-overlay--blocking">
Hello! You can close this notification here:
<button
class="close-button"
@click=${e => e.target.dispatchEvent(new Event('close-overlay', { bubbles: true }))}
>
</button>
</div>
</demo-overlay-system>
</div>
Hello! You can close this notification here:
<button <button
class="close-button" class="close-button"
@click=${e => e.target.dispatchEvent(new Event('close-overlay', { bubbles: true }))} @click=${e => e.target.dispatchEvent(new Event('close-overlay', { bubbles: true }))}
@ -299,7 +289,19 @@ export const isBlocking = () => {
</button> </button>
</div> </div>
</demo-overlay-system> </demo-el-using-overlaymixin>
<demo-el-using-overlaymixin .config=${isBlockingConfig}>
<button slot="invoker">Overlay B: open second</button>
<div slot="content" class="demo-overlay demo-overlay--blocking">
Overlay A is hidden... now close me and see overlay A again.
<button
class="close-button"
@click=${e => e.target.dispatchEvent(new Event('close-overlay', { bubbles: true }))}
>
</button>
</div>
</demo-el-using-overlaymixin>
`; `;
}; };
``` ```
@ -315,7 +317,7 @@ Boolean property. When true, prevents scrolling content that is outside of the `
export const preventsScroll = () => { export const preventsScroll = () => {
const preventsScrollConfig = { preventsScroll: true }; const preventsScrollConfig = { preventsScroll: true };
return html` return html`
<demo-overlay-system .config=${preventsScrollConfig}> <demo-el-using-overlaymixin .config=${preventsScrollConfig}>
<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:
@ -326,7 +328,7 @@ export const preventsScroll = () => {
</button> </button>
</div> </div>
</demo-overlay-system> </demo-el-using-overlaymixin>
`; `;
}; };
``` ```
@ -354,7 +356,7 @@ export const viewportConfig = () => {
viewportConfig: { placement: 'bottom-left' }, viewportConfig: { placement: 'bottom-left' },
}; };
return html` return html`
<demo-overlay-system .config=${viewportConfig}> <demo-el-using-overlaymixin .config=${viewportConfig}>
<button slot="invoker">Click me to open the overlay in the bottom 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:
@ -365,7 +367,7 @@ export const viewportConfig = () => {
</button> </button>
</div> </div>
</demo-overlay-system> </demo-el-using-overlaymixin>
`; `;
}; };
``` ```
@ -434,7 +436,7 @@ export const popperConfig = () => {
border: 1px solid black; border: 1px solid black;
} }
</style> </style>
<demo-overlay-system .config=${popperConfig}> <demo-el-using-overlaymixin .config=${popperConfig}>
<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:
@ -445,7 +447,7 @@ export const popperConfig = () => {
</button> </button>
</div> </div>
</demo-overlay-system> </demo-el-using-overlaymixin>
`; `;
}; };
``` ```

View file

@ -1,65 +0,0 @@
# Systems >> Overlays >> Form Integrations ||60
```js script
import { html } from '@mdjs/mdjs-preview';
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-select-rich.js';
import './assets/demo-overlay-system.mjs';
import './assets/applyDemoOverlayStyles.mjs';
```
## Select Rich
Opening a Rich Select inside a dialog.
```js preview-story
export const selectRich = () => html`
<demo-overlay-system>
<button slot="invoker">Open Dialog</button>
<div slot="content" class="demo-overlay">
<h1>Select Rick example</h1>
<lion-select-rich name="favoriteColor" label="Favorite color">
<lion-option .choiceValue=${'red'}>Red</lion-option>
<lion-option .choiceValue=${'hotpink'} checked>Hotpink</lion-option>
<lion-option .choiceValue=${'teal'}>Teal</lion-option>
</lion-select-rich>
<p>
You can close this dialog here:
<button
class="close-button"
@click=${e => e.target.dispatchEvent(new Event('close-overlay', { bubbles: true }))}
>
</button>
</p>
</div>
</demo-overlay-system>
`;
```
## Input Datepicker
Opening an Input Datepicker inside a dialog.
```js preview-story
export const inputDatepicker = () => html`
<demo-overlay-system>
<button slot="invoker">Open Dialog</button>
<div slot="content" class="demo-overlay">
<h1>Input Datepicker example</h1>
<lion-input-datepicker name="date" label="Date"></lion-input-datepicker>
<p>
You can close this dialog here:
<button
class="close-button"
@click=${e => e.target.dispatchEvent(new Event('close-overlay', { bubbles: true }))}
>
</button>
</p>
</div>
</demo-overlay-system>
`;
```

View file

@ -11,7 +11,7 @@ import {
withModalDialogConfig, withModalDialogConfig,
} from '@lion/ui/overlays.js'; } from '@lion/ui/overlays.js';
import './assets/demo-overlay-system.mjs'; import './assets/demo-el-using-overlaymixin.mjs';
import './assets/demo-overlay-backdrop.mjs'; import './assets/demo-overlay-backdrop.mjs';
import './assets/applyDemoOverlayStyles.mjs'; import './assets/applyDemoOverlayStyles.mjs';
import { ref, createRef } from 'lit/directives/ref.js'; import { ref, createRef } from 'lit/directives/ref.js';
@ -25,7 +25,7 @@ For a detailed rationale, please consult [Rationale](./rationale.md).
```js preview-story ```js preview-story
export const main = () => html` export const main = () => html`
<demo-overlay-system> <demo-el-using-overlaymixin>
<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:
@ -33,7 +33,7 @@ export const main = () => html`
</button> </button>
</div> </div>
</demo-overlay-system> </demo-el-using-overlaymixin>
`; `;
``` ```
@ -132,7 +132,7 @@ render() {
or declaratively in your template with the `.config` property or declaratively in your template with the `.config` property
```html ```html
<demo-overlay-system .config=${{ ...withModalDialogConfig() }}> <demo-el-using-overlaymixin .config=${{ ...withModalDialogConfig() }}>
<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:
@ -142,7 +142,7 @@ or declaratively in your template with the `.config` property
</button> </button>
</div> </div>
</demo-overlay-system> </demo-el-using-overlaymixin>
``` ```
### Backdrop ### Backdrop
@ -155,7 +155,7 @@ The easiest way is declarative. This can be achieved by adding a `<slot name="ba
export const backdrop = () => { export const backdrop = () => {
const responsiveModalDialogConfig = { ...withModalDialogConfig() }; const responsiveModalDialogConfig = { ...withModalDialogConfig() };
return html` return html`
<demo-overlay-system .config=${responsiveModalDialogConfig}> <demo-el-using-overlaymixin .config=${responsiveModalDialogConfig}>
<demo-overlay-backdrop slot="backdrop"></demo-overlay-backdrop> <demo-overlay-backdrop slot="backdrop"></demo-overlay-backdrop>
<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">
@ -166,7 +166,7 @@ export const backdrop = () => {
</button> </button>
</div> </div>
</demo-overlay-system> </demo-el-using-overlaymixin>
`; `;
}; };
``` ```
@ -179,7 +179,7 @@ export const backdropImperative = () => {
const backdropNode = document.createElement('demo-overlay-backdrop'); const backdropNode = document.createElement('demo-overlay-backdrop');
const responsiveModalDialogConfig = { ...withModalDialogConfig(), backdropNode }; const responsiveModalDialogConfig = { ...withModalDialogConfig(), backdropNode };
return html` return html`
<demo-overlay-system .config=${responsiveModalDialogConfig}> <demo-el-using-overlaymixin .config=${responsiveModalDialogConfig}>
<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:
@ -189,7 +189,7 @@ export const backdropImperative = () => {
</button> </button>
</div> </div>
</demo-overlay-system> </demo-el-using-overlaymixin>
`; `;
}; };
``` ```
@ -197,7 +197,7 @@ export const backdropImperative = () => {
#### Backdrop animation #### Backdrop animation
By default our overlay system comes with a backdrop animation. By default our overlay system comes with a backdrop animation.
This will add `global-overlays__backdrop--animation-in` and `global-overlays__backdrop--animation-out` classes to your backdrop node. This will add `overlays__backdrop--animation-in` and `overlays__backdrop--animation-out` classes to your backdrop node.
If you have `placementMode: 'local'` it will replace those `global` strings in the CSS classes with `local`. If you have `placementMode: 'local'` it will replace those `global` strings in the CSS classes with `local`.
It expects from you that you act on these classes in your CSS with an animation. For example if you have your own backdrop webcomponent (to encapsulate styles): It expects from you that you act on these classes in your CSS with an animation. For example if you have your own backdrop webcomponent (to encapsulate styles):
@ -234,7 +234,7 @@ Under the hood, the OverlayController listens to `animationend` event, only then
export const backdropAnimation = () => { export const backdropAnimation = () => {
const responsiveModalDialogConfig = { ...withModalDialogConfig() }; const responsiveModalDialogConfig = { ...withModalDialogConfig() };
return html` return html`
<demo-overlay-system .config=${responsiveModalDialogConfig}> <demo-el-using-overlaymixin .config=${responsiveModalDialogConfig}>
<button slot="invoker">Click me to open the overlay!</button> <button slot="invoker">Click me to open the overlay!</button>
<demo-overlay-backdrop slot="backdrop"></demo-overlay-backdrop> <demo-overlay-backdrop slot="backdrop"></demo-overlay-backdrop>
<div slot="content" class="demo-overlay"> <div slot="content" class="demo-overlay">
@ -245,7 +245,7 @@ export const backdropAnimation = () => {
</button> </button>
</div> </div>
</demo-overlay-system> </demo-el-using-overlaymixin>
`; `;
}; };
``` ```
@ -264,7 +264,7 @@ Drag the viewport under 600px and open the overlay to see the `withBottomSheetCo
export const responsiveSwitching = () => { export const responsiveSwitching = () => {
const responsiveBottomSheetConfig = { ...withBottomSheetConfig() }; const responsiveBottomSheetConfig = { ...withBottomSheetConfig() };
return html` return html`
<demo-overlay-system <demo-el-using-overlaymixin
.config=${responsiveBottomSheetConfig} .config=${responsiveBottomSheetConfig}
@before-opened=${e => { @before-opened=${e => {
if (window.innerWidth >= 600) { if (window.innerWidth >= 600) {
@ -283,7 +283,7 @@ export const responsiveSwitching = () => {
</button> </button>
</div> </div>
</demo-overlay-system> </demo-el-using-overlaymixin>
`; `;
}; };
``` ```
@ -340,7 +340,7 @@ export const responsiveSwitching2 = () => {
case 'bottomsheet': case 'bottomsheet':
return { ...withBottomSheetConfig() }; return { ...withBottomSheetConfig() };
case 'dropdown': case 'dropdown':
return { ...withDropdownConfig(), hasBackdrop: false }; return { ...withDropdownConfig(), hasBackdrop: false, inheritsReferenceWidth: true };
default: default:
return { ...withModalDialogConfig(), hasBackdrop: true }; return { ...withModalDialogConfig(), hasBackdrop: true };
} }
@ -365,7 +365,7 @@ export const responsiveSwitching2 = () => {
</select> </select>
<br /> <br />
<demo-overlay-system ${ref(overlayRef)} .config=${getConfig(selectRef.value?.value)}> <demo-el-using-overlaymixin ${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:
@ -375,7 +375,7 @@ export const responsiveSwitching2 = () => {
</button> </button>
</div> </div>
</demo-overlay-system> </demo-el-using-overlaymixin>
`; `;
}; };
``` ```
@ -401,7 +401,7 @@ export const openedState = () => {
} }
return html` return html`
appState.opened: <span ${ref(myRefs.openedState)}>${appState.opened}</span> appState.opened: <span ${ref(myRefs.openedState)}>${appState.opened}</span>
<demo-overlay-system <demo-el-using-overlaymixin
${ref(myRefs.overlay)} ${ref(myRefs.overlay)}
.opened="${appState.opened}" .opened="${appState.opened}"
@opened-changed=${onOpenClosed} @opened-changed=${onOpenClosed}
@ -415,7 +415,7 @@ export const openedState = () => {
</button> </button>
</div> </div>
</demo-overlay-system> </demo-el-using-overlaymixin>
`; `;
}; };
``` ```
@ -449,7 +449,7 @@ export const interceptingOpenClose = () => {
> >
${blockOverlay} ${blockOverlay}
</button> </button>
<demo-overlay-system <demo-el-using-overlaymixin
${ref(myRefs.overlay)} ${ref(myRefs.overlay)}
@before-closed=${intercept} @before-closed=${intercept}
@before-opened=${intercept} @before-opened=${intercept}
@ -465,7 +465,7 @@ export const interceptingOpenClose = () => {
Hello! You can close this notification here: Hello! You can close this notification here:
<button @click=${() => (myRefs.overlay.value.opened = false)}></button> <button @click=${() => (myRefs.overlay.value.opened = false)}></button>
</div> </div>
</demo-overlay-system> </demo-el-using-overlaymixin>
`; `;
}; };
``` ```
@ -541,7 +541,7 @@ Below an example is shown with the `isBlocking` option, which makes use of the O
export const overlayManager = () => { export const overlayManager = () => {
const hasBackdropConfig = { hasBackdrop: true }; const hasBackdropConfig = { hasBackdrop: true };
return html` return html`
<demo-overlay-system .config=${hasBackdropConfig}> <demo-el-using-overlaymixin .config=${hasBackdropConfig}>
<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:
@ -555,8 +555,11 @@ export const overlayManager = () => {
Click me to open another overlay which is blocking Click me to open another overlay which is blocking
</button> </button>
</div> </div>
</demo-overlay-system> </demo-el-using-overlaymixin>
<demo-overlay-system id="secondOverlay" .config=${{ hasBackdrop: true, isBlocking: true }}> <demo-el-using-overlaymixin
id="secondOverlay"
.config=${{ hasBackdrop: true, isBlocking: true }}
>
<div slot="content" class="demo-overlay demo-overlay--second"> <div slot="content" class="demo-overlay demo-overlay--second">
Hello! You can close this notification here: Hello! You can close this notification here:
<button <button
@ -565,7 +568,7 @@ export const overlayManager = () => {
</button> </button>
</div> </div>
</demo-overlay-system> </demo-el-using-overlaymixin>
`; `;
}; };
``` ```
@ -580,7 +583,7 @@ Here is the example below
export const localBackdrop = () => { export const localBackdrop = () => {
const localBackdropConfig = { placementMode: 'local' }; const localBackdropConfig = { placementMode: 'local' };
return html` return html`
<demo-overlay-system .config=${localBackdropConfig}> <demo-el-using-overlaymixin .config=${localBackdropConfig}>
<demo-overlay-backdrop slot="backdrop"></demo-overlay-backdrop> <demo-overlay-backdrop slot="backdrop"></demo-overlay-backdrop>
<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">
@ -591,7 +594,7 @@ export const localBackdrop = () => {
</button> </button>
</div> </div>
</demo-overlay-system> </demo-el-using-overlaymixin>
`; `;
}; };
``` ```
@ -604,10 +607,10 @@ It's also possible to compose a nested construction by moving around dom nodes.
```js preview-story ```js preview-story
export const nestedOverlays = () => { export const nestedOverlays = () => {
return html` return html`
<demo-overlay-system .config="${withModalDialogConfig()}"> <demo-el-using-overlaymixin .config="${withModalDialogConfig()}">
<div slot="content" id="mainContent" class="demo-overlay"> <div slot="content" id="mainContent" class="demo-overlay">
open nested overlay: open nested overlay:
<demo-overlay-system .config="${withModalDialogConfig()}"> <demo-el-using-overlaymixin .config="${withModalDialogConfig()}">
<div slot="content" id="nestedContent" class="demo-overlay"> <div slot="content" id="nestedContent" class="demo-overlay">
Nested content Nested content
<button <button
@ -617,7 +620,7 @@ export const nestedOverlays = () => {
</button> </button>
</div> </div>
<button slot="invoker" id="nestedInvoker">nested invoker button</button> <button slot="invoker" id="nestedInvoker">nested invoker button</button>
</demo-overlay-system> </demo-el-using-overlaymixin>
<button <button
@click=${e => e.target.dispatchEvent(new Event('close-overlay', { bubbles: true }))} @click=${e => e.target.dispatchEvent(new Event('close-overlay', { bubbles: true }))}
> >
@ -625,7 +628,7 @@ export const nestedOverlays = () => {
</button> </button>
</div> </div>
<button slot="invoker" id="mainInvoker">invoker button</button> <button slot="invoker" id="mainInvoker">invoker button</button>
</demo-overlay-system> </demo-el-using-overlaymixin>
`; `;
}; };
``` ```

View file

@ -12,7 +12,7 @@ describe('lion-dialog', () => {
// 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'); const globalRootNode = document.querySelector('.overlays');
if (globalRootNode) { if (globalRootNode) {
globalRootNode.innerHTML = ''; globalRootNode.innerHTML = '';
} }

View file

@ -11,7 +11,7 @@ async function combineLocalizeImports(importPromises) {
} }
return combinedResult; return combinedResult;
} }
/* eslint-disable import/no-extraneous-dependencies */
export const localizeNamespaceLoader = /** @param {string} locale */ locale => { export const localizeNamespaceLoader = /** @param {string} locale */ locale => {
switch (locale) { switch (locale) {
case 'bg-BG': case 'bg-BG':

View file

@ -685,9 +685,7 @@ describe('<lion-input-datepicker>', () => {
await myElObj.openCalendar(); await myElObj.openCalendar();
expect(el.hasArrow).to.be.false; expect(el.hasArrow).to.be.false;
expect( expect(
myElObj.overlayController.contentNode.classList.contains( myElObj.overlayController.contentNode.classList.contains('overlays__overlay--bottom-sheet'),
'global-overlays__overlay--bottom-sheet',
),
'Datepicker does not get rendered as bottom sheet', 'Datepicker does not get rendered as bottom sheet',
).to.be.false; ).to.be.false;
}); });

View file

@ -2,7 +2,7 @@ import { EventTargetShim } from '@lion/ui/core.js';
import { adoptStyles } from 'lit'; 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'; import { overlayShadowDomStyle } from './overlayShadowDomStyle.js';
/** /**
* @typedef {import('@lion/ui/types/overlays.js').OverlayConfig} OverlayConfig * @typedef {import('@lion/ui/types/overlays.js').OverlayConfig} OverlayConfig
@ -239,7 +239,6 @@ export class OverlayController extends EventTargetShim {
viewportConfig: { viewportConfig: {
placement: 'center', placement: 'center',
}, },
zIndex: 9999, zIndex: 9999,
}; };
@ -495,7 +494,7 @@ export class OverlayController extends EventTargetShim {
* @type {OverlayConfig} * @type {OverlayConfig}
* @private * @private
*/ */
this.__prevConfig = this.config || {}; this.__prevConfig = this.config;
/** @type {OverlayConfig} */ /** @type {OverlayConfig} */
this.config = { this.config = {
@ -571,10 +570,9 @@ export class OverlayController extends EventTargetShim {
if (!OverlayController.popperModule) { if (!OverlayController.popperModule) {
OverlayController.popperModule = preloadPopper(); OverlayController.popperModule = preloadPopper();
} }
} else {
const rootNode = /** @type {ShadowRoot} */ (this.contentWrapperNode.getRootNode());
adoptStyles(rootNode, [...(rootNode.adoptedStyleSheets || []), globalOverlaysStyle]);
} }
const rootNode = /** @type {ShadowRoot} */ (this.contentWrapperNode.getRootNode());
adoptStyles(rootNode, [...(rootNode.adoptedStyleSheets || []), overlayShadowDomStyle]);
this._handleFeatures({ phase: 'init' }); this._handleFeatures({ phase: 'init' });
} }
@ -612,10 +610,11 @@ export class OverlayController extends EventTargetShim {
// A11y will depend on the type of overlay and is arranged on contentNode level. // 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 // Also see: https://www.scottohara.me/blog/2019/03/05/open-dialog.html
wrappingDialogElement.setAttribute('role', 'none'); wrappingDialogElement.setAttribute('role', 'none');
wrappingDialogElement.setAttribute('data-overlay-outer-wrapper', '');
// N.B. position: fixed is needed to escape out of 'overflow: hidden' // 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 // We give a high z-index for non-modal dialogs, so that we at least win from all siblings of our
// parent stacking context // parent stacking context
wrappingDialogElement.style.cssText = `display:none; background-image: none; border-style: none; padding: 0; z-index: ${this.config.zIndex};`; wrappingDialogElement.style.cssText = `display:none; z-index: ${this.config.zIndex};`;
this.__wrappingDialogNode = wrappingDialogElement; this.__wrappingDialogNode = wrappingDialogElement;
/** /**
@ -788,16 +787,16 @@ export class OverlayController extends EventTargetShim {
*/ */
async _handlePosition({ phase }) { async _handlePosition({ phase }) {
if (this.placementMode === 'global') { if (this.placementMode === 'global') {
const placementClass = `global-overlays__overlay-container--${this.viewportConfig.placement}`; const placementClass = `overlays__overlay-container--${this.viewportConfig.placement}`;
if (phase === 'show') { if (phase === 'show') {
this.contentWrapperNode.classList.add('global-overlays__overlay-container'); this.contentWrapperNode.classList.add('overlays__overlay-container');
this.contentWrapperNode.classList.add(placementClass); this.contentWrapperNode.classList.add(placementClass);
this.contentNode.classList.add('global-overlays__overlay'); this.contentNode.classList.add('overlays__overlay');
} else if (phase === 'hide') { } else if (phase === 'hide') {
this.contentWrapperNode.classList.remove('global-overlays__overlay-container'); this.contentWrapperNode.classList.remove('overlays__overlay-container');
this.contentWrapperNode.classList.remove(placementClass); this.contentWrapperNode.classList.remove(placementClass);
this.contentNode.classList.remove('global-overlays__overlay'); this.contentNode.classList.remove('overlays__overlay');
} }
} else if (this.placementMode === 'local' && phase === 'show') { } else if (this.placementMode === 'local' && phase === 'show') {
/** /**
@ -918,7 +917,7 @@ export class OverlayController extends EventTargetShim {
if (!backdropNode) { if (!backdropNode) {
return; return;
} }
backdropNode.classList.remove(`global-overlays__backdrop--animation-in`); backdropNode.classList.remove(`overlays__backdrop--animation-in`);
} }
/** /**
@ -939,7 +938,7 @@ 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(`global-overlays__backdrop--animation-in`); showConfig.backdropNode.classList.add(`overlays__backdrop--animation-in`);
} }
} }
@ -1049,7 +1048,7 @@ export class OverlayController extends EventTargetShim {
if (!this.config?.backdropNode) { if (!this.config?.backdropNode) {
this.__backdropNode = document.createElement('div'); this.__backdropNode = document.createElement('div');
// If backdropNode existed in config, styles are applied by implementing party // If backdropNode existed in config, styles are applied by implementing party
this.__backdropNode.classList.add(`global-overlays__backdrop`); this.__backdropNode.classList.add(`overlays__backdrop`);
} }
// @ts-ignore // @ts-ignore
this.__wrappingDialogNode.prepend(this.backdropNode); this.__wrappingDialogNode.prepend(this.backdropNode);
@ -1058,7 +1057,7 @@ export class OverlayController extends EventTargetShim {
break; break;
} }
case 'show': case 'show':
this.backdropNode.classList.add(`global-overlays__backdrop--visible`); this.backdropNode.classList.add(`overlays__backdrop--visible`);
this.__hasActiveBackdrop = true; this.__hasActiveBackdrop = true;
break; break;
case 'hide': case 'hide':

View file

@ -90,10 +90,10 @@ export const OverlayMixinImplementation = superclass =>
...this.config, // user provided (e.g. in template) ...this.config, // user provided (e.g. in template)
popperConfig: { popperConfig: {
...(overlayConfig.popperConfig || {}), ...(overlayConfig.popperConfig || {}),
...(this.config.popperConfig || {}), ...(this.config?.popperConfig || {}),
modifiers: [ modifiers: [
...(overlayConfig.popperConfig?.modifiers || []), ...(overlayConfig.popperConfig?.modifiers || []),
...(this.config.popperConfig?.modifiers || []), ...(this.config?.popperConfig?.modifiers || []),
], ],
}, },
}); });

View file

@ -3,7 +3,7 @@
* @typedef {import('./OverlayController.js').OverlayController} OverlayController * @typedef {import('./OverlayController.js').OverlayController} OverlayController
*/ */
import { globalOverlaysStyle } from './globalOverlaysStyle.js'; import { overlayDocumentStyle } from './overlayDocumentStyle.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?
@ -24,8 +24,8 @@ export const _browserDetection = {
export class OverlaysManager { export class OverlaysManager {
static __createGlobalStyleNode() { static __createGlobalStyleNode() {
const styleTag = document.createElement('style'); const styleTag = document.createElement('style');
styleTag.setAttribute('data-global-overlays', ''); styleTag.setAttribute('data-overlays', '');
styleTag.textContent = /** @type {CSSResult} */ (globalOverlaysStyle).cssText; styleTag.textContent = /** @type {CSSResult} */ (overlayDocumentStyle).cssText;
document.head.appendChild(styleTag); document.head.appendChild(styleTag);
return styleTag; return styleTag;
} }
@ -184,27 +184,32 @@ export class OverlaysManager {
requestToPreventScroll() { requestToPreventScroll() {
const { isIOS, isMacSafari } = _browserDetection; const { isIOS, isMacSafari } = _browserDetection;
// no check as classList will dedupe it anyways // no check as classList will dedupe it anyways
document.body.classList.add('global-overlays-scroll-lock'); document.body.classList.add('overlays-scroll-lock');
if (isIOS || isMacSafari) { if (isIOS || isMacSafari) {
// iOS and safar for mac have issues with overlays with input fields. This is fixed by applying // iOS and safar for mac have issues with overlays with input fields. This is fixed by applying
// position: fixed to the body. As a side effect, this will scroll the body to the top. // position: fixed to the body. As a side effect, this will scroll the body to the top.
document.body.classList.add('global-overlays-scroll-lock-ios-fix'); document.body.classList.add('overlays-scroll-lock-ios-fix');
} }
if (isIOS) { if (isIOS) {
document.documentElement.classList.add('global-overlays-scroll-lock-ios-fix'); document.documentElement.classList.add('overlays-scroll-lock-ios-fix');
} }
} }
requestToEnableScroll() { requestToEnableScroll() {
const hasOpenSiblingThatPreventsScroll = this.shownList.some(
ctrl => ctrl.preventsScroll === true,
);
if (hasOpenSiblingThatPreventsScroll) {
return;
}
const { isIOS, isMacSafari } = _browserDetection; const { isIOS, isMacSafari } = _browserDetection;
if (!this.shownList.some(ctrl => ctrl.preventsScroll === true)) { document.body.classList.remove('overlays-scroll-lock');
document.body.classList.remove('global-overlays-scroll-lock'); if (isIOS || isMacSafari) {
if (isIOS || isMacSafari) { document.body.classList.remove('overlays-scroll-lock-ios-fix');
document.body.classList.remove('global-overlays-scroll-lock-ios-fix'); }
} if (isIOS) {
if (isIOS) { document.documentElement.classList.remove('overlays-scroll-lock-ios-fix');
document.documentElement.classList.remove('global-overlays-scroll-lock-ios-fix');
}
} }
} }

View file

@ -1,146 +0,0 @@
import { css } from 'lit';
export const globalOverlaysStyle = css`
.global-overlays {
position: fixed;
z-index: 200;
}
.global-overlays__overlay-container {
display: flex;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
.global-overlays__overlay-container::backdrop {
display: none;
}
.global-overlays__overlay-container--top-left {
justify-content: flex-start;
align-items: flex-start;
}
.global-overlays__overlay-container--top {
justify-content: center;
align-items: flex-start;
}
.global-overlays__overlay-container--top-right {
justify-content: flex-end;
align-items: flex-start;
}
.global-overlays__overlay-container--right {
justify-content: flex-end;
align-items: center;
}
.global-overlays__overlay-container--bottom-left {
justify-content: flex-start;
align-items: flex-end;
}
.global-overlays__overlay-container--bottom {
justify-content: center;
align-items: flex-end;
}
.global-overlays__overlay-container--bottom-right {
justify-content: flex-end;
align-items: flex-end;
}
.global-overlays__overlay-container--left {
justify-content: flex-start;
align-items: center;
}
.global-overlays__overlay-container--center {
justify-content: center;
align-items: center;
}
.global-overlays__overlay--bottom-sheet {
width: 100%;
}
::slotted(.global-overlays__overlay),
.global-overlays__overlay {
pointer-events: auto;
}
.global-overlays__backdrop {
content: '';
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: -1;
background-color: #333333;
filter: opacity(30%);
display: none;
}
.global-overlays__backdrop--visible {
display: block;
}
.global-overlays__backdrop--animation-in {
animation: global-overlays-backdrop-fade-in 300ms;
opacity: 0.3;
}
.global-overlays__backdrop--animation-out {
animation: global-overlays-backdrop-fade-out 300ms;
opacity: 0;
}
@keyframes global-overlays-backdrop-fade-in {
from {
opacity: 0;
}
}
@keyframes global-overlays-backdrop-fade-out {
from {
opacity: 0.3;
}
}
body > *[inert] {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
pointer-events: none;
}
body.global-overlays-scroll-lock {
overflow: hidden;
}
body.global-overlays-scroll-lock-ios-fix {
position: fixed;
width: 100%;
}
html.global-overlays-scroll-lock-ios-fix {
height: 100vh;
}
@media screen and (prefers-reduced-motion: reduce) {
.global-overlays .global-overlays__backdrop--animation-in {
animation: global-overlays-backdrop-fade-in 1ms;
}
.global-overlays .global-overlays__backdrop--animation-out {
animation: global-overlays-backdrop-fade-out 1ms;
}
}
`;

View file

@ -0,0 +1,24 @@
import { css } from 'lit';
export const overlayDocumentStyle = css`
body > *[inert] {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
pointer-events: none;
}
body.overlays-scroll-lock {
overflow: hidden;
}
body.overlays-scroll-lock-ios-fix {
position: fixed;
width: 100%;
}
html.overlays-scroll-lock-ios-fix {
height: 100vh;
}
`;

View file

@ -0,0 +1,138 @@
import { css } from 'lit';
export const overlayShadowDomStyle = css`
.overlays {
position: fixed;
z-index: 200;
}
.overlays__overlay-container {
display: flex;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
.overlays__overlay-container::backdrop {
display: none;
}
.overlays__overlay-container--top-left {
justify-content: flex-start;
align-items: flex-start;
}
.overlays__overlay-container--top {
justify-content: center;
align-items: flex-start;
}
.overlays__overlay-container--top-right {
justify-content: flex-end;
align-items: flex-start;
}
.overlays__overlay-container--right {
justify-content: flex-end;
align-items: center;
}
.overlays__overlay-container--bottom-left {
justify-content: flex-start;
align-items: flex-end;
}
.overlays__overlay-container--bottom {
justify-content: center;
align-items: flex-end;
}
.overlays__overlay-container--bottom-right {
justify-content: flex-end;
align-items: flex-end;
}
.overlays__overlay-container--left {
justify-content: flex-start;
align-items: center;
}
.overlays__overlay-container--center {
justify-content: center;
align-items: center;
}
.overlays__overlay--bottom-sheet {
width: 100%;
}
::slotted(.overlays__overlay),
.overlays__overlay {
pointer-events: auto;
}
.overlays__backdrop {
content: '';
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: -1;
background-color: #333333;
display: none;
}
.overlays__backdrop--visible {
display: block;
}
.overlays__backdrop--animation-in {
animation: overlays-backdrop-fade-in 300ms;
opacity: 0.3;
}
.overlays__backdrop--animation-out {
animation: overlays-backdrop-fade-out 300ms;
opacity: 0;
}
@keyframes overlays-backdrop-fade-in {
from {
opacity: 0;
}
}
@keyframes overlays-backdrop-fade-out {
from {
opacity: 0.3;
}
}
@media screen and (prefers-reduced-motion: reduce) {
.overlays .overlays__backdrop--animation-in {
animation: overlays-backdrop-fade-in 1ms;
}
.overlays .overlays__backdrop--animation-out {
animation: overlays-backdrop-fade-out 1ms;
}
}
dialog[data-overlay-outer-wrapper] {
background-image: none;
border-style: none;
padding: 0px;
}
/**
* We don't want to use pseudo el ::backdrop.
* We have our own, that creates more flexibility wrt scrolling etc.
*/
dialog[data-overlay-outer-wrapper]::backdrop {
display: none;
}
`;

View file

@ -96,7 +96,7 @@ export function runOverlayMixinSuite({ tagString, tag, suffix = '' }) {
) )
); );
// For now, we skip this test for MacSafari, since the body.global-overlays-scroll-lock-ios-fix // For now, we skip this test for MacSafari, since the body.overlays-scroll-lock-ios-fix
// class results in a scrollbar when preventsScroll is true. // class results in a scrollbar when preventsScroll is true.
// However, fully functioning interacive elements (input fields) in the dialog are more important // However, fully functioning interacive elements (input fields) in the dialog are more important
if (_browserDetection.isMacSafari && elWithBigParent._overlayCtrl.preventsScroll) { if (_browserDetection.isMacSafari && elWithBigParent._overlayCtrl.preventsScroll) {

View file

@ -20,8 +20,7 @@ import { simulateTab } from '../src/utils/simulate-tab.js';
* @typedef {import('../types/OverlayConfig.js').ViewportPlacement} ViewportPlacement * @typedef {import('../types/OverlayConfig.js').ViewportPlacement} ViewportPlacement
*/ */
const wrappingDialogNodeStyle = const wrappingDialogNodeStyle = 'display: none; z-index: 9999;';
'display: none; background-image: none; border-style: none; padding: 0px; z-index: 9999;';
/** /**
* @param {HTMLElement} node * @param {HTMLElement} node
@ -178,8 +177,7 @@ describe('OverlayController', () => {
}); });
it('succeeds when passing a content node that was created "online"', async () => { it('succeeds when passing a content node that was created "online"', async () => {
const contentNode = document.createElement('div'); const contentNode = /** @type {HTMLElement} */ (fixtureSync('<div>'));
document.body.appendChild(contentNode);
const overlay = new OverlayController({ const overlay = new OverlayController({
...withLocalTestConfig(), ...withLocalTestConfig(),
contentNode, contentNode,
@ -236,7 +234,7 @@ describe('OverlayController', () => {
// The total dom structure created... // The total dom structure created...
expect(el).shadowDom.to.equal(` expect(el).shadowDom.to.equal(`
<dialog open="" role="none" style="${wrappingDialogNodeStyle}"> <dialog data-overlay-outer-wrapper="" open="" role="none" style="${wrappingDialogNodeStyle}">
<div data-id="content-wrapper"> <div data-id="content-wrapper">
<slot name="content"> <slot name="content">
</slot> </slot>
@ -263,7 +261,7 @@ describe('OverlayController', () => {
// The total dom structure created... // The total dom structure created...
expect(el).lightDom.to.equal(` expect(el).lightDom.to.equal(`
<dialog open="" role="none" style="${wrappingDialogNodeStyle}"> <dialog data-overlay-outer-wrapper="" open="" role="none" style="${wrappingDialogNodeStyle}">
<div data-id="content-wrapper"> <div data-id="content-wrapper">
<div id="content">non projected</div> <div id="content">non projected</div>
</div> </div>
@ -304,7 +302,7 @@ describe('OverlayController', () => {
// The total dom structure created... // The total dom structure created...
expect(el).shadowDom.to.equal(` expect(el).shadowDom.to.equal(`
<dialog open="" role="none" style="${wrappingDialogNodeStyle}"> <dialog data-overlay-outer-wrapper="" open="" role="none" style="${wrappingDialogNodeStyle}">
<div data-id="content-wrapper"> <div data-id="content-wrapper">
<slot name="content"></slot> <slot name="content"></slot>
</div> </div>
@ -347,7 +345,7 @@ describe('OverlayController', () => {
// The total dom structure created... // The total dom structure created...
expect(el).shadowDom.to.equal(` expect(el).shadowDom.to.equal(`
<dialog open="" role="none" style="${wrappingDialogNodeStyle}"> <dialog data-overlay-outer-wrapper="" open="" role="none" style="${wrappingDialogNodeStyle}">
<div data-id="content-wrapper"> <div data-id="content-wrapper">
<div id="arrow"></div> <div id="arrow"></div>
<slot name="content"></slot> <slot name="content"></slot>
@ -414,8 +412,8 @@ describe('OverlayController', () => {
// The total dom structure created... // The total dom structure created...
expect(el).shadowDom.to.equal( expect(el).shadowDom.to.equal(
` `
<dialog open="" role="none" style="${wrappingDialogNodeStyle}"> <dialog data-overlay-outer-wrapper="" open="" role="none" style="${wrappingDialogNodeStyle}">
<div class="global-overlays__backdrop"></div> <div class="overlays__backdrop"></div>
<div data-id="content-wrapper"> <div data-id="content-wrapper">
<slot name="content"> <slot name="content">
</slot> </slot>
@ -565,6 +563,18 @@ describe('OverlayController', () => {
document.dispatchEvent(new KeyboardEvent('keyup', { key: 'Escape' })); document.dispatchEvent(new KeyboardEvent('keyup', { key: 'Escape' }));
expect(ctrl.isShown).to.be.true; expect(ctrl.isShown).to.be.true;
}); });
it('does not hide when [escape] is pressed with modal <dialog> and "hidesOnEsc" is false', async () => {
const ctrl = new OverlayController({
...withGlobalTestConfig(),
trapsKeyboardFocus: true,
hidesOnEsc: false,
});
await ctrl.show();
ctrl.contentNode.dispatchEvent(new KeyboardEvent('keyup', { key: 'Escape' }));
await aTimeout(0);
expect(ctrl.isShown).to.be.true;
});
}); });
describe('hidesOnOutsideEsc', () => { describe('hidesOnOutsideEsc', () => {
@ -929,10 +939,10 @@ describe('OverlayController', () => {
}); });
await ctrl.show(); await ctrl.show();
expect(getComputedStyle(document.body).overflow).to.equal('hidden'); expect(Array.from(document.body.classList)).to.contain('overlays-scroll-lock');
await ctrl.hide(); await ctrl.hide();
expect(getComputedStyle(document.body).overflow).to.equal('visible'); expect(Array.from(document.body.classList)).to.not.contain('overlays-scroll-lock');
}); });
it('keeps preventing of scrolling when multiple overlays are opened and closed', async () => { it('keeps preventing of scrolling when multiple overlays are opened and closed', async () => {
@ -948,7 +958,7 @@ describe('OverlayController', () => {
await ctrl0.show(); await ctrl0.show();
await ctrl1.show(); await ctrl1.show();
await ctrl1.hide(); await ctrl1.hide();
expect(getComputedStyle(document.body).overflow).to.equal('hidden'); expect(Array.from(document.body.classList)).to.contain('overlays-scroll-lock');
}); });
}); });
@ -975,7 +985,7 @@ describe('OverlayController', () => {
hasBackdrop: true, hasBackdrop: true,
}); });
await controllerWithBackdrop.show(); await controllerWithBackdrop.show();
expect(controllerWithBackdrop.backdropNode).to.have.class('global-overlays__backdrop'); expect(controllerWithBackdrop.backdropNode).to.have.class('overlays__backdrop');
}); });
it('reenables the backdrop when shown/hidden/shown', async () => { it('reenables the backdrop when shown/hidden/shown', async () => {
@ -984,10 +994,10 @@ describe('OverlayController', () => {
hasBackdrop: true, hasBackdrop: true,
}); });
await ctrl.show(); await ctrl.show();
expect(ctrl.backdropNode).to.have.class('global-overlays__backdrop'); expect(ctrl.backdropNode).to.have.class('overlays__backdrop');
await ctrl.hide(); await ctrl.hide();
await ctrl.show(); await ctrl.show();
expect(ctrl.backdropNode).to.have.class('global-overlays__backdrop'); expect(ctrl.backdropNode).to.have.class('overlays__backdrop');
}); });
it('adds and stacks backdrops if .hasBackdrop is enabled', async () => { it('adds and stacks backdrops if .hasBackdrop is enabled', async () => {
@ -996,14 +1006,14 @@ describe('OverlayController', () => {
hasBackdrop: true, hasBackdrop: true,
}); });
await ctrl0.show(); await ctrl0.show();
expect(ctrl0.backdropNode).to.have.class('global-overlays__backdrop'); expect(ctrl0.backdropNode).to.have.class('overlays__backdrop');
const ctrl1 = new OverlayController({ const ctrl1 = new OverlayController({
...withGlobalTestConfig(), ...withGlobalTestConfig(),
hasBackdrop: false, hasBackdrop: false,
}); });
await ctrl1.show(); await ctrl1.show();
expect(ctrl0.backdropNode).to.have.class('global-overlays__backdrop'); expect(ctrl0.backdropNode).to.have.class('overlays__backdrop');
expect(ctrl1.backdropNode).to.be.undefined; expect(ctrl1.backdropNode).to.be.undefined;
const ctrl2 = new OverlayController({ const ctrl2 = new OverlayController({
@ -1012,9 +1022,9 @@ describe('OverlayController', () => {
}); });
await ctrl2.show(); await ctrl2.show();
expect(ctrl0.backdropNode).to.have.class('global-overlays__backdrop'); expect(ctrl0.backdropNode).to.have.class('overlays__backdrop');
expect(ctrl1.backdropNode).to.be.undefined; expect(ctrl1.backdropNode).to.be.undefined;
expect(ctrl2.backdropNode).to.have.class('global-overlays__backdrop'); expect(ctrl2.backdropNode).to.have.class('overlays__backdrop');
}); });
}); });
@ -1404,15 +1414,11 @@ describe('OverlayController', () => {
}); });
ctrl.show(); ctrl.show();
expect( expect(ctrl.contentWrapperNode.classList.contains('overlays__overlay-container--center'));
ctrl.contentWrapperNode.classList.contains('global-overlays__overlay-container--center'),
);
expect(ctrl.isShown).to.be.true; expect(ctrl.isShown).to.be.true;
ctrl.updateConfig({ viewportConfig: { placement: 'top-right' } }); ctrl.updateConfig({ viewportConfig: { placement: 'top-right' } });
expect( expect(ctrl.contentWrapperNode.classList.contains('overlays__overlay-container--top-right'));
ctrl.contentWrapperNode.classList.contains('global-overlays__overlay-container--top-right'),
);
expect(ctrl.isShown).to.be.true; expect(ctrl.isShown).to.be.true;
}); });
}); });

View file

@ -28,14 +28,14 @@ describe('OverlaysManager', () => {
}); });
it('provides global stylesheet for arrangement of body scroll', () => { it('provides global stylesheet for arrangement of body scroll', () => {
expect(document.head.querySelectorAll('[data-global-overlays]').length).to.equal(1); expect(document.head.querySelectorAll('[data-overlays]').length).to.equal(1);
}); });
it('provides .teardown() for cleanup', () => { it('provides .teardown() for cleanup', () => {
expect(document.head.querySelector('[data-global-overlays=""]')).not.be.undefined; expect(document.head.querySelector('[data-overlays=""]')).not.be.undefined;
mngr.teardown(); mngr.teardown();
expect(document.head.querySelector('[data-global-overlays=""]')).be.null; expect(document.head.querySelector('[data-overlays=""]')).be.null;
// safety check via private access (do not use this) // safety check via private access (do not use this)
expect(OverlaysManager.__globalStyleNode).to.be.undefined; expect(OverlaysManager.__globalStyleNode).to.be.undefined;
@ -122,22 +122,18 @@ describe('OverlaysManager', () => {
}); });
describe('When initialized with "preventsScroll: true"', () => { describe('When initialized with "preventsScroll: true"', () => {
it('adds class "global-overlays-scroll-lock-ios-fix" to body and html on iOS', async () => { it('adds class "overlays-scroll-lock-ios-fix" to body and html on iOS', async () => {
mockIOS(); mockIOS();
const dialog = new OverlayController({ ...defaultOptions, preventsScroll: true }, mngr); const dialog = new OverlayController({ ...defaultOptions, preventsScroll: true }, mngr);
await dialog.show(); await dialog.show();
expect(Array.from(document.body.classList)).to.contain( expect(Array.from(document.body.classList)).to.contain('overlays-scroll-lock-ios-fix');
'global-overlays-scroll-lock-ios-fix',
);
expect(Array.from(document.documentElement.classList)).to.contain( expect(Array.from(document.documentElement.classList)).to.contain(
'global-overlays-scroll-lock-ios-fix', 'overlays-scroll-lock-ios-fix',
); );
await dialog.hide(); await dialog.hide();
expect(Array.from(document.body.classList)).to.not.contain( expect(Array.from(document.body.classList)).to.not.contain('overlays-scroll-lock-ios-fix');
'global-overlays-scroll-lock-ios-fix',
);
expect(Array.from(document.documentElement.classList)).to.not.contain( expect(Array.from(document.documentElement.classList)).to.not.contain(
'global-overlays-scroll-lock-ios-fix', 'overlays-scroll-lock-ios-fix',
); );
// When we are not iOS nor MacSafari // When we are not iOS nor MacSafari
@ -146,30 +142,24 @@ describe('OverlaysManager', () => {
const dialog2 = new OverlayController({ ...defaultOptions, preventsScroll: true }, mngr); const dialog2 = new OverlayController({ ...defaultOptions, preventsScroll: true }, mngr);
await dialog2.show(); await dialog2.show();
expect(Array.from(document.body.classList)).to.not.contain( expect(Array.from(document.body.classList)).to.not.contain('overlays-scroll-lock-ios-fix');
'global-overlays-scroll-lock-ios-fix',
);
expect(Array.from(document.documentElement.classList)).to.not.contain( expect(Array.from(document.documentElement.classList)).to.not.contain(
'global-overlays-scroll-lock-ios-fix', 'overlays-scroll-lock-ios-fix',
); );
}); });
it('adds class "global-overlays-scroll-lock-ios-fix" to body on MacSafari', async () => { it('adds class "overlays-scroll-lock-ios-fix" to body on MacSafari', async () => {
mockMacSafari(); mockMacSafari();
const dialog = new OverlayController({ ...defaultOptions, preventsScroll: true }, mngr); const dialog = new OverlayController({ ...defaultOptions, preventsScroll: true }, mngr);
await dialog.show(); await dialog.show();
expect(Array.from(document.body.classList)).to.contain( expect(Array.from(document.body.classList)).to.contain('overlays-scroll-lock-ios-fix');
'global-overlays-scroll-lock-ios-fix',
);
expect(Array.from(document.documentElement.classList)).to.not.contain( expect(Array.from(document.documentElement.classList)).to.not.contain(
'global-overlays-scroll-lock-ios-fix', 'overlays-scroll-lock-ios-fix',
); );
await dialog.hide(); await dialog.hide();
expect(Array.from(document.body.classList)).to.not.contain( expect(Array.from(document.body.classList)).to.not.contain('overlays-scroll-lock-ios-fix');
'global-overlays-scroll-lock-ios-fix',
);
expect(Array.from(document.documentElement.classList)).to.not.contain( expect(Array.from(document.documentElement.classList)).to.not.contain(
'global-overlays-scroll-lock-ios-fix', 'overlays-scroll-lock-ios-fix',
); );
}); });
}); });

View file

@ -24,9 +24,8 @@ describe('Global Positioning', () => {
...withDefaultGlobalConfig(), ...withDefaultGlobalConfig(),
}); });
await ctrl.show(); await ctrl.show();
expect( expect(ctrl.contentWrapperNode.classList.contains('overlays__overlay-container--center')).to
ctrl.contentWrapperNode.classList.contains('global-overlays__overlay-container--center'), .be.true;
).to.be.true;
}); });
it('positions relative to the viewport ', async () => { it('positions relative to the viewport ', async () => {
@ -51,7 +50,7 @@ describe('Global Positioning', () => {
await ctrl.show(); await ctrl.show();
expect( expect(
ctrl.contentWrapperNode.classList.contains( ctrl.contentWrapperNode.classList.contains(
`global-overlays__overlay-container--${viewportPlacement}`, `overlays__overlay-container--${viewportPlacement}`,
), ),
).to.be.true; ).to.be.true;
}); });

View file

@ -1,8 +1,23 @@
import { Options } from '@popperjs/core'; import { Options } from '@popperjs/core';
export interface OverlayConfig { export interface OverlayConfig {
/** Determines the connection point in DOM (body vs next to invoker). */ // Positioning
/** Determines the positioning anchore (viewport vs invokerNode/referenceNode). */
placementMode?: 'global' | 'local' | undefined; placementMode?: 'global' | 'local' | undefined;
/** Popper configuration. Will be used when placementMode is 'local' */
popperConfig?: Partial<Options>;
/** Viewport positioning configuration. Will be used when placementMode is 'global' */
viewportConfig?: ViewportConfig;
/** Hides other overlays when multiple are opened (currently exclusive to globalOverlayController) */
isBlocking?: boolean;
/** Will align contentNode with referenceNode (invokerNode by default) for local overlays. Usually needed for dropdowns. 'max' will prevent contentNode from exceeding width of referenceNode, 'min' guarantees that contentNode will be at least as wide as referenceNode. 'full' will make sure that the invoker width always is the same. */
inheritsReferenceWidth?: 'max' | 'full' | 'min' | 'none';
/** Change the default of 9999 */
zIndex?: number;
// Elements
/** The interactive element (usually a button) invoking the dialog or tooltip */ /** The interactive element (usually a button) invoking the dialog or tooltip */
invokerNode?: HTMLElement; invokerNode?: HTMLElement;
/** The element that is used to position the overlay content relative to. Usually, this is the same element as invokerNode. Should only be provided when invokerNode should not be positioned against */ /** The element that is used to position the overlay content relative to. Usually, this is the same element as invokerNode. Should only be provided when invokerNode should not be positioned against */
@ -15,10 +30,14 @@ export interface OverlayConfig {
backdropNode?: HTMLElement; backdropNode?: HTMLElement;
/** The element that should be called `.focus()` on after dialog closes */ /** The element that should be called `.focus()` on after dialog closes */
elementToFocusAfterHide?: HTMLElement; elementToFocusAfterHide?: HTMLElement;
// Backdrop
/** 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 multiple are opened (currently exclusive to globalOverlayController) */
isBlocking?: boolean; // User interaction
/** 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;
/** Rotates tab, implicitly set when 'isModal' */ /** Rotates tab, implicitly set when 'isModal' */
@ -29,8 +48,9 @@ export interface OverlayConfig {
hidesOnOutsideClick?: boolean; hidesOnOutsideClick?: boolean;
/** Hides the overlay when pressing esc, even when contentNode has no focus */ /** Hides the overlay when pressing esc, even when contentNode has no focus */
hidesOnOutsideEsc?: boolean; hidesOnOutsideEsc?: boolean;
/** Will align contentNode with referenceNode (invokerNode by default) for local overlays. Usually needed for dropdowns. 'max' will prevent contentNode from exceeding width of referenceNode, 'min' guarantees that contentNode will be at least as wide as referenceNode. 'full' will make sure that the invoker width always is the same. */
inheritsReferenceWidth?: 'max' | 'full' | 'min' | 'none'; // Accessibility
/** /**
* For non `isTooltip`: * For non `isTooltip`:
* - sets aria-expanded="true/false" and aria-haspopup="true" on invokerNode * - sets aria-expanded="true/false" and aria-haspopup="true" on invokerNode
@ -42,17 +62,13 @@ export interface OverlayConfig {
* - sets role="tooltip" and aria-labelledby/aria-describedby on the content * - sets role="tooltip" and aria-labelledby/aria-describedby on the content
*/ */
handlesAccessibility?: boolean; handlesAccessibility?: boolean;
// Tooltip
/** Has a totally different interaction- and accessibility pattern from all other overlays. Will behave as role="tooltip" element instead of a role="dialog" element */ /** Has a totally different interaction- and accessibility pattern from all other overlays. Will behave as role="tooltip" element instead of a role="dialog" element */
isTooltip?: boolean; isTooltip?: boolean;
/** By default, the tooltip content is a 'description' for the invoker (uses aria-describedby) Setting this property to 'label' makes the content function as a label (via aria-labelledby) */ /** By default, the tooltip content is a 'description' for the invoker (uses aria-describedby) Setting this property to 'label' makes the content function as a label (via aria-labelledby) */
invokerRelation?: 'label' | 'description'; invokerRelation?: 'label' | 'description';
/** Popper configuration. Will be used when placementMode is 'local' */
popperConfig?: Partial<Options>;
/** Viewport configuration. Will be used when placementMode is 'global' */
viewportConfig?: ViewportConfig;
/** Change the default of 9999 */
zIndex?: number;
/** render a div instead of dialog */ /** render a div instead of dialog */
_noDialogEl?: Boolean; _noDialogEl?: Boolean;

View file

@ -14,7 +14,7 @@ const applyDemoOverlayStyles = () => {
`; `;
const styleTag = document.createElement('style'); const styleTag = document.createElement('style');
styleTag.setAttribute('data-demo-global-overlays', ''); styleTag.setAttribute('data-demo-overlays', '');
styleTag.textContent = demoOverlaysStyle.cssText; styleTag.textContent = demoOverlaysStyle.cssText;
document.head.appendChild(styleTag); document.head.appendChild(styleTag);
}; };

View file

@ -2,9 +2,9 @@ import { html, LitElement } from 'lit';
import { OverlayMixin } from '@lion/ui/overlays.js'; import { OverlayMixin } from '@lion/ui/overlays.js';
/** /**
* @typedef {import('../types/OverlayConfig.js').OverlayConfig} OverlayConfig * @typedef {import('@lion/ui/types/overlays.js').OverlayConfig} OverlayConfig
*/ */
class DemoOverlaySystem extends OverlayMixin(LitElement) { class DemoElUsingOverlayMixin extends OverlayMixin(LitElement) {
// eslint-disable-next-line class-methods-use-this // eslint-disable-next-line class-methods-use-this
_defineOverlayConfig() { _defineOverlayConfig() {
return /** @type {OverlayConfig} */ ({ return /** @type {OverlayConfig} */ ({
@ -35,8 +35,7 @@ class DemoOverlaySystem extends OverlayMixin(LitElement) {
<div id="overlay-content-node-wrapper"> <div id="overlay-content-node-wrapper">
<slot name="content"></slot> <slot name="content"></slot>
</div> </div>
<div>popup is ${this.opened ? 'opened' : 'closed'}</div>
`; `;
} }
} }
customElements.define('demo-overlay-system', DemoOverlaySystem); customElements.define('demo-el-using-overlaymixin', DemoElUsingOverlayMixin);

View file

@ -13,6 +13,7 @@ class DemoOverlayBackdrop extends LitElement {
height: 100%; height: 100%;
background-color: grey; background-color: grey;
opacity: 0.3; opacity: 0.3;
position: fixed;
} }
:host(.local-overlays__backdrop--visible) { :host(.local-overlays__backdrop--visible) {

View file

@ -1,6 +1,5 @@
export { OverlaysManager } from '../components/overlays/src/OverlaysManager.js'; export { OverlaysManager } from '../components/overlays/src/OverlaysManager.js';
export { globalOverlaysStyle } from '../components/overlays/src/globalOverlaysStyle.js';
export { OverlayController } from '../components/overlays/src/OverlayController.js'; export { OverlayController } from '../components/overlays/src/OverlayController.js';
export { OverlayMixin } from '../components/overlays/src/OverlayMixin.js'; export { OverlayMixin } from '../components/overlays/src/OverlayMixin.js';
export { ArrowMixin } from '../components/overlays/src/ArrowMixin.js'; export { ArrowMixin } from '../components/overlays/src/ArrowMixin.js';