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>
<lion-dialog>
<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:
<button
class="close-button"
class="demo-dialog-content__close-button"
@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';
```
- 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.
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, although this is discouraged as a practice.
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).
## Changing 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).
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 `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`.

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;
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,61 +22,14 @@ import './src/slots-dialog-content.js';
</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
```js preview-story
export const placementOverrides = () => {
const dialog = placement => html`
<lion-dialog .config="${{ viewportConfig: { placement } }}">
const dialog = placement => {
const cfg = { viewportConfig: { placement } };
return html`
<lion-dialog .config="${cfg}">
<button slot="invoker">Dialog ${placement}</button>
<div slot="content" class="dialog demo-box">
Hello! You can close this notification here:
@ -89,6 +42,7 @@ export const placementOverrides = () => {
</div>
</lion-dialog>
`;
};
return html`
<style>
${demoStyle}
@ -116,23 +70,24 @@ Configuration passed to `config` property:
No backdrop, hides on escape, prevents scrolling while opened, and focuses the body when hiding.
```js preview-story
export const otherOverrides = () => html`
<style>
${demoStyle}
</style>
<lion-dialog
.config=${{
export const otherOverrides = () => {
const cfg = {
hasBackdrop: false,
hidesOnEscape: true,
preventsScroll: true,
elementToFocusAfterHide: document.body,
}}
>
};
return html`
<style>
${demoStyle}
</style>
<lion-dialog .config="${cfg}">
<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:
<button
class="close-button"
class="demo-dialog-content__close-button"
@click=${e => e.target.dispatchEvent(new Event('close-overlay', { bubbles: true }))}
>
@ -140,6 +95,7 @@ export const otherOverrides = () => html`
</div>
</lion-dialog>
`;
};
```
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
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>`;
```
## 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.

View file

@ -15,7 +15,7 @@ const applyDemoOverlayStyles = () => {
`;
const styleTag = document.createElement('style');
styleTag.setAttribute('data-demo-global-overlays', '');
styleTag.setAttribute('data-demo-overlays', '');
styleTag.textContent = demoOverlaysStyle.cssText;
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
*/
class DemoOverlaySystem extends OverlayMixin(LitElement) {
class DemoElUsingOverlayMixin extends OverlayMixin(LitElement) {
// eslint-disable-next-line class-methods-use-this
_defineOverlayConfig() {
return /** @type {OverlayConfig} */ ({
@ -36,12 +36,10 @@ class DemoOverlaySystem extends OverlayMixin(LitElement) {
<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);
customElements.define('demo-el-using-overlaymixin', DemoElUsingOverlayMixin);
class DemoOverlay extends OverlayMixin(LitElement) {
static get styles() {

View file

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

View file

@ -3,7 +3,7 @@
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';
import './demo-el-using-overlaymixin.mjs';
/**
* @typedef {import('@lion/ui/types/overlays.js').OverlayConfig} OverlayConfig
@ -44,8 +44,7 @@ class DemoOverlayEl extends OverlayMixin(LitElement) {
height: 100%;
}
:host([simulate-viewport])
#overlay-content-node-wrapper.global-overlays__overlay-container {
:host([simulate-viewport]) #overlay-content-node-wrapper.overlays__overlay-container {
position: absolute;
}

View file

@ -2,7 +2,7 @@
```js script
import { html } from '@mdjs/mdjs-preview';
import './assets/demo-overlay-system.mjs';
import './assets/demo-el-using-overlaymixin.mjs';
import './assets/applyDemoOverlayStyles.mjs';
```
@ -14,7 +14,7 @@ The `OverlayMixin` exposes these options via `.config`.
Either `'local'` or `'global'`.
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
@ -28,7 +28,7 @@ export const placementLocal = () => {
border: 1px solid black;
}
</style>
<demo-overlay-system .config=${placementModeLocalConfig}>
<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:
@ -39,7 +39,7 @@ export const placementLocal = () => {
</button>
</div>
</demo-overlay-system>
</demo-el-using-overlaymixin>
`;
};
```
@ -50,7 +50,7 @@ export const placementLocal = () => {
export const placementGlobal = () => {
const placementModeGlobalConfig = { placementMode: 'global' };
return html`
<demo-overlay-system .config=${placementModeGlobalConfig}>
<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:
@ -61,7 +61,7 @@ export const placementGlobal = () => {
</button>
</div>
</demo-overlay-system>
</demo-el-using-overlaymixin>
`;
};
```
@ -91,12 +91,12 @@ export const isTooltip = () => {
const tooltipConfig = { placementMode: 'local', isTooltip: true, handlesAccessibility: true };
return html`
<demo-overlay-system id="tooltip" .config=${tooltipConfig}>
<demo-el-using-overlaymixin id="tooltip" .config=${tooltipConfig}>
<button slot="invoker" @mouseenter=${showTooltip} @mouseleave=${hideTooltip}>
Hover me to open the tooltip!
</button>
<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 = () => {
const trapsKeyboardFocusConfig = { trapsKeyboardFocus: true };
return html`
<demo-overlay-system .config=${trapsKeyboardFocusConfig}>
<demo-el-using-overlaymixin .config=${trapsKeyboardFocusConfig}>
<button slot="invoker">Click me to open the overlay!</button>
<div slot="content" class="demo-overlay">
<div><a href="#">A focusable anchor</a></div>
@ -127,7 +127,7 @@ export const trapsKeyboardFocus = () => {
</button>
</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 = () => {
const hidesOnEscConfig = { hidesOnEsc: true };
return html`
<demo-overlay-system .config=${hidesOnEscConfig}>
<demo-el-using-overlaymixin .config=${hidesOnEscConfig}>
<button slot="invoker">Click me to open the overlay!</button>
<div slot="content" class="demo-overlay">
Hello! You can close this notification here:
@ -151,7 +151,7 @@ export const hidesOnEsc = () => {
</button>
</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 = () => {
const hidesOnEscConfig = { hidesOnOutsideEsc: true };
return html`
<demo-overlay-system .config=${hidesOnEscConfig}>
<demo-el-using-overlaymixin .config=${hidesOnEscConfig}>
<button slot="invoker">Click me to open the overlay!</button>
<div slot="content" class="demo-overlay">
Hello! You can close this notification here:
@ -175,7 +175,7 @@ export const hidesOnOutsideEsc = () => {
</button>
</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 = () => {
const hidesOnOutsideClickConfig = { hidesOnOutsideClick: true };
return html`
<demo-overlay-system .config=${hidesOnOutsideClickConfig}>
<demo-el-using-overlaymixin .config=${hidesOnOutsideClickConfig}>
<button slot="invoker">Click me to open the overlay!</button>
<div slot="content" class="demo-overlay">
<label for="myInput">Clicking this label should not trigger close</label>
@ -200,7 +200,7 @@ export const hidesOnOutsideClick = () => {
</button>
</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
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`
<demo-overlay-system .config=${elementToFocusAfterHideConfig}>
<demo-el-using-overlaymixin .config=${elementToFocusAfterHideConfig}>
<button slot="invoker">Click me to open the overlay!</button>
<div slot="content" class="demo-overlay">
Hello! You can close this notification here:
@ -226,7 +229,8 @@ export const elementToFocusAfterHide = () => {
</button>
</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.
> 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
`.global-overlays .global-overlays__backdrop--animation-in` and
`.global-overlays .global-overlays__backdrop--animation-out` css selector.
`.overlays .overlays__backdrop--animation-in` and
`.overlays .overlays__backdrop--animation-out` css selector.
This currently only supports CSS Animations, because it relies on the `animationend` event to add/remove classes.
```js preview-story
export const hasBackdrop = () => {
const hasBackdropConfig = { hasBackdrop: true };
return html`
<demo-overlay-system .config=${hasBackdropConfig}>
<demo-el-using-overlaymixin .config=${hasBackdropConfig}>
<button slot="invoker">Click me to open the overlay!</button>
<div slot="content" class="demo-overlay">
Hello! You can close this notification here:
@ -261,7 +265,7 @@ export const hasBackdrop = () => {
</button>
</div>
</demo-overlay-system>
</demo-el-using-overlaymixin>
`;
};
```
@ -274,14 +278,22 @@ Boolean property. When true, will block other overlays.
export const isBlocking = () => {
const isBlockingConfig = { hasBackdrop: true, isBlocking: true };
return html`
<demo-overlay-system>
<button slot="invoker">Click me to open the overlay!</button>
<div slot="content" class="demo-overlay">
<div>
<demo-overlay-system .config=${isBlockingConfig}>
<button slot="invoker">Click me to open another overlay which is blocking</button>
<demo-el-using-overlaymixin>
<button slot="invoker">Overlay A: open first</button>
<div slot="content" class="demo-overlay" style="width:200px;">
This overlay gets closed when overlay B gets opened
<button
class="close-button"
@click=${e => e.target.dispatchEvent(new Event('close-overlay', { bubbles: true }))}
>
</button>
</div>
</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">
Hello! You can close this notification here:
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 }))}
@ -289,17 +301,7 @@ export const isBlocking = () => {
</button>
</div>
</demo-overlay-system>
</div>
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>
</demo-el-using-overlaymixin>
`;
};
```
@ -315,7 +317,7 @@ Boolean property. When true, prevents scrolling content that is outside of the `
export const preventsScroll = () => {
const preventsScrollConfig = { preventsScroll: true };
return html`
<demo-overlay-system .config=${preventsScrollConfig}>
<demo-el-using-overlaymixin .config=${preventsScrollConfig}>
<button slot="invoker">Click me to open the overlay!</button>
<div slot="content" class="demo-overlay">
Hello! You can close this notification here:
@ -326,7 +328,7 @@ export const preventsScroll = () => {
</button>
</div>
</demo-overlay-system>
</demo-el-using-overlaymixin>
`;
};
```
@ -354,7 +356,7 @@ export const viewportConfig = () => {
viewportConfig: { placement: 'bottom-left' },
};
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>
<div slot="content" class="demo-overlay">
Hello! You can close this notification here:
@ -365,7 +367,7 @@ export const viewportConfig = () => {
</button>
</div>
</demo-overlay-system>
</demo-el-using-overlaymixin>
`;
};
```
@ -434,7 +436,7 @@ export const popperConfig = () => {
border: 1px solid black;
}
</style>
<demo-overlay-system .config=${popperConfig}>
<demo-el-using-overlaymixin .config=${popperConfig}>
<button slot="invoker">Click me to open the overlay!</button>
<div slot="content" class="demo-overlay">
Hello! You can close this notification here:
@ -445,7 +447,7 @@ export const popperConfig = () => {
</button>
</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,
} 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/applyDemoOverlayStyles.mjs';
import { ref, createRef } from 'lit/directives/ref.js';
@ -25,7 +25,7 @@ For a detailed rationale, please consult [Rationale](./rationale.md).
```js preview-story
export const main = () => html`
<demo-overlay-system>
<demo-el-using-overlaymixin>
<button slot="invoker">Click me to open the overlay!</button>
<div slot="content" class="demo-overlay">
Hello! You can close this notification here:
@ -33,7 +33,7 @@ export const main = () => html`
</button>
</div>
</demo-overlay-system>
</demo-el-using-overlaymixin>
`;
```
@ -132,7 +132,7 @@ render() {
or declaratively in your template with the `.config` property
```html
<demo-overlay-system .config=${{ ...withModalDialogConfig() }}>
<demo-el-using-overlaymixin .config=${{ ...withModalDialogConfig() }}>
<button slot="invoker">Click me to open the overlay!</button>
<div slot="content" class="demo-overlay">
Hello! You can close this notification here:
@ -142,7 +142,7 @@ or declaratively in your template with the `.config` property
</button>
</div>
</demo-overlay-system>
</demo-el-using-overlaymixin>
```
### Backdrop
@ -155,7 +155,7 @@ The easiest way is declarative. This can be achieved by adding a `<slot name="ba
export const backdrop = () => {
const responsiveModalDialogConfig = { ...withModalDialogConfig() };
return html`
<demo-overlay-system .config=${responsiveModalDialogConfig}>
<demo-el-using-overlaymixin .config=${responsiveModalDialogConfig}>
<demo-overlay-backdrop slot="backdrop"></demo-overlay-backdrop>
<button slot="invoker">Click me to open the overlay!</button>
<div slot="content" class="demo-overlay">
@ -166,7 +166,7 @@ export const backdrop = () => {
</button>
</div>
</demo-overlay-system>
</demo-el-using-overlaymixin>
`;
};
```
@ -179,7 +179,7 @@ export const backdropImperative = () => {
const backdropNode = document.createElement('demo-overlay-backdrop');
const responsiveModalDialogConfig = { ...withModalDialogConfig(), backdropNode };
return html`
<demo-overlay-system .config=${responsiveModalDialogConfig}>
<demo-el-using-overlaymixin .config=${responsiveModalDialogConfig}>
<button slot="invoker">Click me to open the overlay!</button>
<div slot="content" class="demo-overlay">
Hello! You can close this notification here:
@ -189,7 +189,7 @@ export const backdropImperative = () => {
</button>
</div>
</demo-overlay-system>
</demo-el-using-overlaymixin>
`;
};
```
@ -197,7 +197,7 @@ export const backdropImperative = () => {
#### 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`.
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 = () => {
const responsiveModalDialogConfig = { ...withModalDialogConfig() };
return html`
<demo-overlay-system .config=${responsiveModalDialogConfig}>
<demo-el-using-overlaymixin .config=${responsiveModalDialogConfig}>
<button slot="invoker">Click me to open the overlay!</button>
<demo-overlay-backdrop slot="backdrop"></demo-overlay-backdrop>
<div slot="content" class="demo-overlay">
@ -245,7 +245,7 @@ export const backdropAnimation = () => {
</button>
</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 = () => {
const responsiveBottomSheetConfig = { ...withBottomSheetConfig() };
return html`
<demo-overlay-system
<demo-el-using-overlaymixin
.config=${responsiveBottomSheetConfig}
@before-opened=${e => {
if (window.innerWidth >= 600) {
@ -283,7 +283,7 @@ export const responsiveSwitching = () => {
</button>
</div>
</demo-overlay-system>
</demo-el-using-overlaymixin>
`;
};
```
@ -340,7 +340,7 @@ export const responsiveSwitching2 = () => {
case 'bottomsheet':
return { ...withBottomSheetConfig() };
case 'dropdown':
return { ...withDropdownConfig(), hasBackdrop: false };
return { ...withDropdownConfig(), hasBackdrop: false, inheritsReferenceWidth: true };
default:
return { ...withModalDialogConfig(), hasBackdrop: true };
}
@ -365,7 +365,7 @@ export const responsiveSwitching2 = () => {
</select>
<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>
<div slot="content" class="demo-overlay">
Hello! You can close this notification here:
@ -375,7 +375,7 @@ export const responsiveSwitching2 = () => {
</button>
</div>
</demo-overlay-system>
</demo-el-using-overlaymixin>
`;
};
```
@ -401,7 +401,7 @@ export const openedState = () => {
}
return html`
appState.opened: <span ${ref(myRefs.openedState)}>${appState.opened}</span>
<demo-overlay-system
<demo-el-using-overlaymixin
${ref(myRefs.overlay)}
.opened="${appState.opened}"
@opened-changed=${onOpenClosed}
@ -415,7 +415,7 @@ export const openedState = () => {
</button>
</div>
</demo-overlay-system>
</demo-el-using-overlaymixin>
`;
};
```
@ -449,7 +449,7 @@ export const interceptingOpenClose = () => {
>
${blockOverlay}
</button>
<demo-overlay-system
<demo-el-using-overlaymixin
${ref(myRefs.overlay)}
@before-closed=${intercept}
@before-opened=${intercept}
@ -465,7 +465,7 @@ export const interceptingOpenClose = () => {
Hello! You can close this notification here:
<button @click=${() => (myRefs.overlay.value.opened = false)}></button>
</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 = () => {
const hasBackdropConfig = { hasBackdrop: true };
return html`
<demo-overlay-system .config=${hasBackdropConfig}>
<demo-el-using-overlaymixin .config=${hasBackdropConfig}>
<button slot="invoker">Click me to open the overlay!</button>
<div slot="content" class="demo-overlay">
Hello! You can close this notification here:
@ -555,8 +555,11 @@ export const overlayManager = () => {
Click me to open another overlay which is blocking
</button>
</div>
</demo-overlay-system>
<demo-overlay-system id="secondOverlay" .config=${{ hasBackdrop: true, isBlocking: true }}>
</demo-el-using-overlaymixin>
<demo-el-using-overlaymixin
id="secondOverlay"
.config=${{ hasBackdrop: true, isBlocking: true }}
>
<div slot="content" class="demo-overlay demo-overlay--second">
Hello! You can close this notification here:
<button
@ -565,7 +568,7 @@ export const overlayManager = () => {
</button>
</div>
</demo-overlay-system>
</demo-el-using-overlaymixin>
`;
};
```
@ -580,7 +583,7 @@ Here is the example below
export const localBackdrop = () => {
const localBackdropConfig = { placementMode: 'local' };
return html`
<demo-overlay-system .config=${localBackdropConfig}>
<demo-el-using-overlaymixin .config=${localBackdropConfig}>
<demo-overlay-backdrop slot="backdrop"></demo-overlay-backdrop>
<button slot="invoker">Click me to open the overlay!</button>
<div slot="content" class="demo-overlay">
@ -591,7 +594,7 @@ export const localBackdrop = () => {
</button>
</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
export const nestedOverlays = () => {
return html`
<demo-overlay-system .config="${withModalDialogConfig()}">
<demo-el-using-overlaymixin .config="${withModalDialogConfig()}">
<div slot="content" id="mainContent" class="demo-overlay">
open nested overlay:
<demo-overlay-system .config="${withModalDialogConfig()}">
<demo-el-using-overlaymixin .config="${withModalDialogConfig()}">
<div slot="content" id="nestedContent" class="demo-overlay">
Nested content
<button
@ -617,7 +620,7 @@ export const nestedOverlays = () => {
</button>
</div>
<button slot="invoker" id="nestedInvoker">nested invoker button</button>
</demo-overlay-system>
</demo-el-using-overlaymixin>
<button
@click=${e => e.target.dispatchEvent(new Event('close-overlay', { bubbles: true }))}
>
@ -625,7 +628,7 @@ export const nestedOverlays = () => {
</button>
</div>
<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...
// Not sure why this "bug" happens...
beforeEach(() => {
const globalRootNode = document.querySelector('.global-overlays');
const globalRootNode = document.querySelector('.overlays');
if (globalRootNode) {
globalRootNode.innerHTML = '';
}

View file

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

View file

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

View file

@ -2,7 +2,7 @@ import { EventTargetShim } from '@lion/ui/core.js';
import { adoptStyles } from 'lit';
import { overlays } from './singleton.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
@ -239,7 +239,6 @@ export class OverlayController extends EventTargetShim {
viewportConfig: {
placement: 'center',
},
zIndex: 9999,
};
@ -495,7 +494,7 @@ export class OverlayController extends EventTargetShim {
* @type {OverlayConfig}
* @private
*/
this.__prevConfig = this.config || {};
this.__prevConfig = this.config;
/** @type {OverlayConfig} */
this.config = {
@ -571,10 +570,9 @@ export class OverlayController extends EventTargetShim {
if (!OverlayController.popperModule) {
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' });
}
@ -612,10 +610,11 @@ export class OverlayController extends EventTargetShim {
// 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');
wrappingDialogElement.setAttribute('data-overlay-outer-wrapper', '');
// 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};`;
wrappingDialogElement.style.cssText = `display:none; z-index: ${this.config.zIndex};`;
this.__wrappingDialogNode = wrappingDialogElement;
/**
@ -788,16 +787,16 @@ export class OverlayController extends EventTargetShim {
*/
async _handlePosition({ phase }) {
if (this.placementMode === 'global') {
const placementClass = `global-overlays__overlay-container--${this.viewportConfig.placement}`;
const placementClass = `overlays__overlay-container--${this.viewportConfig.placement}`;
if (phase === 'show') {
this.contentWrapperNode.classList.add('global-overlays__overlay-container');
this.contentWrapperNode.classList.add('overlays__overlay-container');
this.contentWrapperNode.classList.add(placementClass);
this.contentNode.classList.add('global-overlays__overlay');
this.contentNode.classList.add('overlays__overlay');
} 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.contentNode.classList.remove('global-overlays__overlay');
this.contentNode.classList.remove('overlays__overlay');
}
} else if (this.placementMode === 'local' && phase === 'show') {
/**
@ -918,7 +917,7 @@ export class OverlayController extends EventTargetShim {
if (!backdropNode) {
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 });
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) {
this.__backdropNode = document.createElement('div');
// 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
this.__wrappingDialogNode.prepend(this.backdropNode);
@ -1058,7 +1057,7 @@ export class OverlayController extends EventTargetShim {
break;
}
case 'show':
this.backdropNode.classList.add(`global-overlays__backdrop--visible`);
this.backdropNode.classList.add(`overlays__backdrop--visible`);
this.__hasActiveBackdrop = true;
break;
case 'hide':

View file

@ -90,10 +90,10 @@ export const OverlayMixinImplementation = superclass =>
...this.config, // user provided (e.g. in template)
popperConfig: {
...(overlayConfig.popperConfig || {}),
...(this.config.popperConfig || {}),
...(this.config?.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
*/
import { globalOverlaysStyle } from './globalOverlaysStyle.js';
import { overlayDocumentStyle } from './overlayDocumentStyle.js';
// Export this as protected var, so that we can easily mock it in tests
// TODO: combine with browserDetection of core?
@ -24,8 +24,8 @@ export const _browserDetection = {
export class OverlaysManager {
static __createGlobalStyleNode() {
const styleTag = document.createElement('style');
styleTag.setAttribute('data-global-overlays', '');
styleTag.textContent = /** @type {CSSResult} */ (globalOverlaysStyle).cssText;
styleTag.setAttribute('data-overlays', '');
styleTag.textContent = /** @type {CSSResult} */ (overlayDocumentStyle).cssText;
document.head.appendChild(styleTag);
return styleTag;
}
@ -184,27 +184,32 @@ export class OverlaysManager {
requestToPreventScroll() {
const { isIOS, isMacSafari } = _browserDetection;
// 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) {
// 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.
document.body.classList.add('global-overlays-scroll-lock-ios-fix');
document.body.classList.add('overlays-scroll-lock-ios-fix');
}
if (isIOS) {
document.documentElement.classList.add('global-overlays-scroll-lock-ios-fix');
document.documentElement.classList.add('overlays-scroll-lock-ios-fix');
}
}
requestToEnableScroll() {
const hasOpenSiblingThatPreventsScroll = this.shownList.some(
ctrl => ctrl.preventsScroll === true,
);
if (hasOpenSiblingThatPreventsScroll) {
return;
}
const { isIOS, isMacSafari } = _browserDetection;
if (!this.shownList.some(ctrl => ctrl.preventsScroll === true)) {
document.body.classList.remove('global-overlays-scroll-lock');
document.body.classList.remove('overlays-scroll-lock');
if (isIOS || isMacSafari) {
document.body.classList.remove('global-overlays-scroll-lock-ios-fix');
document.body.classList.remove('overlays-scroll-lock-ios-fix');
}
if (isIOS) {
document.documentElement.classList.remove('global-overlays-scroll-lock-ios-fix');
}
document.documentElement.classList.remove('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.
// However, fully functioning interacive elements (input fields) in the dialog are more important
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
*/
const wrappingDialogNodeStyle =
'display: none; background-image: none; border-style: none; padding: 0px; z-index: 9999;';
const wrappingDialogNodeStyle = 'display: none; z-index: 9999;';
/**
* @param {HTMLElement} node
@ -178,8 +177,7 @@ describe('OverlayController', () => {
});
it('succeeds when passing a content node that was created "online"', async () => {
const contentNode = document.createElement('div');
document.body.appendChild(contentNode);
const contentNode = /** @type {HTMLElement} */ (fixtureSync('<div>'));
const overlay = new OverlayController({
...withLocalTestConfig(),
contentNode,
@ -236,7 +234,7 @@ describe('OverlayController', () => {
// The total dom structure created...
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">
<slot name="content">
</slot>
@ -263,7 +261,7 @@ describe('OverlayController', () => {
// The total dom structure created...
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 id="content">non projected</div>
</div>
@ -304,7 +302,7 @@ describe('OverlayController', () => {
// The total dom structure created...
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">
<slot name="content"></slot>
</div>
@ -347,7 +345,7 @@ describe('OverlayController', () => {
// The total dom structure created...
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 id="arrow"></div>
<slot name="content"></slot>
@ -414,8 +412,8 @@ describe('OverlayController', () => {
// The total dom structure created...
expect(el).shadowDom.to.equal(
`
<dialog open="" role="none" style="${wrappingDialogNodeStyle}">
<div class="global-overlays__backdrop"></div>
<dialog data-overlay-outer-wrapper="" open="" role="none" style="${wrappingDialogNodeStyle}">
<div class="overlays__backdrop"></div>
<div data-id="content-wrapper">
<slot name="content">
</slot>
@ -565,6 +563,18 @@ describe('OverlayController', () => {
document.dispatchEvent(new KeyboardEvent('keyup', { key: 'Escape' }));
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', () => {
@ -929,10 +939,10 @@ describe('OverlayController', () => {
});
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();
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 () => {
@ -948,7 +958,7 @@ describe('OverlayController', () => {
await ctrl0.show();
await ctrl1.show();
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,
});
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 () => {
@ -984,10 +994,10 @@ describe('OverlayController', () => {
hasBackdrop: true,
});
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.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 () => {
@ -996,14 +1006,14 @@ describe('OverlayController', () => {
hasBackdrop: true,
});
await ctrl0.show();
expect(ctrl0.backdropNode).to.have.class('global-overlays__backdrop');
expect(ctrl0.backdropNode).to.have.class('overlays__backdrop');
const ctrl1 = new OverlayController({
...withGlobalTestConfig(),
hasBackdrop: false,
});
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;
const ctrl2 = new OverlayController({
@ -1012,9 +1022,9 @@ describe('OverlayController', () => {
});
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(ctrl2.backdropNode).to.have.class('global-overlays__backdrop');
expect(ctrl2.backdropNode).to.have.class('overlays__backdrop');
});
});
@ -1404,15 +1414,11 @@ describe('OverlayController', () => {
});
ctrl.show();
expect(
ctrl.contentWrapperNode.classList.contains('global-overlays__overlay-container--center'),
);
expect(ctrl.contentWrapperNode.classList.contains('overlays__overlay-container--center'));
expect(ctrl.isShown).to.be.true;
ctrl.updateConfig({ viewportConfig: { placement: 'top-right' } });
expect(
ctrl.contentWrapperNode.classList.contains('global-overlays__overlay-container--top-right'),
);
expect(ctrl.contentWrapperNode.classList.contains('overlays__overlay-container--top-right'));
expect(ctrl.isShown).to.be.true;
});
});

View file

@ -28,14 +28,14 @@ describe('OverlaysManager', () => {
});
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', () => {
expect(document.head.querySelector('[data-global-overlays=""]')).not.be.undefined;
expect(document.head.querySelector('[data-overlays=""]')).not.be.undefined;
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)
expect(OverlaysManager.__globalStyleNode).to.be.undefined;
@ -122,22 +122,18 @@ describe('OverlaysManager', () => {
});
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();
const dialog = new OverlayController({ ...defaultOptions, preventsScroll: true }, mngr);
await dialog.show();
expect(Array.from(document.body.classList)).to.contain(
'global-overlays-scroll-lock-ios-fix',
);
expect(Array.from(document.body.classList)).to.contain('overlays-scroll-lock-ios-fix');
expect(Array.from(document.documentElement.classList)).to.contain(
'global-overlays-scroll-lock-ios-fix',
'overlays-scroll-lock-ios-fix',
);
await dialog.hide();
expect(Array.from(document.body.classList)).to.not.contain(
'global-overlays-scroll-lock-ios-fix',
);
expect(Array.from(document.body.classList)).to.not.contain('overlays-scroll-lock-ios-fix');
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
@ -146,30 +142,24 @@ describe('OverlaysManager', () => {
const dialog2 = new OverlayController({ ...defaultOptions, preventsScroll: true }, mngr);
await dialog2.show();
expect(Array.from(document.body.classList)).to.not.contain(
'global-overlays-scroll-lock-ios-fix',
);
expect(Array.from(document.body.classList)).to.not.contain('overlays-scroll-lock-ios-fix');
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();
const dialog = new OverlayController({ ...defaultOptions, preventsScroll: true }, mngr);
await dialog.show();
expect(Array.from(document.body.classList)).to.contain(
'global-overlays-scroll-lock-ios-fix',
);
expect(Array.from(document.body.classList)).to.contain('overlays-scroll-lock-ios-fix');
expect(Array.from(document.documentElement.classList)).to.not.contain(
'global-overlays-scroll-lock-ios-fix',
'overlays-scroll-lock-ios-fix',
);
await dialog.hide();
expect(Array.from(document.body.classList)).to.not.contain(
'global-overlays-scroll-lock-ios-fix',
);
expect(Array.from(document.body.classList)).to.not.contain('overlays-scroll-lock-ios-fix');
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(),
});
await ctrl.show();
expect(
ctrl.contentWrapperNode.classList.contains('global-overlays__overlay-container--center'),
).to.be.true;
expect(ctrl.contentWrapperNode.classList.contains('overlays__overlay-container--center')).to
.be.true;
});
it('positions relative to the viewport ', async () => {
@ -51,7 +50,7 @@ describe('Global Positioning', () => {
await ctrl.show();
expect(
ctrl.contentWrapperNode.classList.contains(
`global-overlays__overlay-container--${viewportPlacement}`,
`overlays__overlay-container--${viewportPlacement}`,
),
).to.be.true;
});

View file

@ -1,8 +1,23 @@
import { Options } from '@popperjs/core';
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;
/** 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 */
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 */
@ -15,10 +30,14 @@ export interface OverlayConfig {
backdropNode?: HTMLElement;
/** The element that should be called `.focus()` on after dialog closes */
elementToFocusAfterHide?: HTMLElement;
// Backdrop
/** Whether it should have a backdrop (currently exclusive to globalOverlayController) */
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) */
preventsScroll?: boolean;
/** Rotates tab, implicitly set when 'isModal' */
@ -29,8 +48,9 @@ export interface OverlayConfig {
hidesOnOutsideClick?: boolean;
/** Hides the overlay when pressing esc, even when contentNode has no focus */
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`:
* - 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
*/
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 */
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) */
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 */
_noDialogEl?: Boolean;

View file

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

View file

@ -2,9 +2,9 @@ import { html, LitElement } from 'lit';
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
_defineOverlayConfig() {
return /** @type {OverlayConfig} */ ({
@ -35,8 +35,7 @@ class DemoOverlaySystem extends OverlayMixin(LitElement) {
<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);
customElements.define('demo-el-using-overlaymixin', DemoElUsingOverlayMixin);

View file

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

View file

@ -1,6 +1,5 @@
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 { OverlayMixin } from '../components/overlays/src/OverlayMixin.js';
export { ArrowMixin } from '../components/overlays/src/ArrowMixin.js';