diff --git a/docs/components/dialog/overview.md b/docs/components/dialog/overview.md index 34bf02e94..f4db20562 100644 --- a/docs/components/dialog/overview.md +++ b/docs/components/dialog/overview.md @@ -17,10 +17,10 @@ export const main = () => html` -
+
Hello! You can close this dialog here: - - -`; -``` - -## Content with slots - -```js preview-story -export const slotsContent = () => html` - - - - -

Some Stuff

-

I am in the actions slot

-
-
-`; -``` - -## 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` - - - - - -`; -``` - ## Placement overrides ```js preview-story export const placementOverrides = () => { - const dialog = placement => html` - - -
- Hello! You can close this notification here: - -
-
- `; + const dialog = placement => { + const cfg = { viewportConfig: { placement } }; + return html` + + +
+ Hello! You can close this notification here: + +
+
+ `; + }; return html` - - -
- Hello! You can close this dialog here: - -
-
-`; +export const otherOverrides = () => { + const cfg = { + hasBackdrop: false, + hidesOnEscape: true, + preventsScroll: true, + elementToFocusAfterHide: document.body, + }; + + return html` + + + +
+ Hello! You can close this dialog here: + +
+
+ `; +}; ``` Configuration passed to `config` property: diff --git a/docs/fundamentals/systems/overlays/_configuration-positioning.md b/docs/fundamentals/systems/overlays/_configuration-positioning.md new file mode 100644 index 000000000..b850ac9c2 --- /dev/null +++ b/docs/fundamentals/systems/overlays/_configuration-positioning.md @@ -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 `` 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``; +``` + +## 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``; +``` + +## placementMode + +The `placementMode` property determines the positioning of the `contentNode`: + +- next to its reference node: `local` +- relative to the viewport: `global` + +### Local + + + +```js story +export const placementLocal = () => { + const placementModeLocalConfig = { placementMode: 'local' }; + return html` + + +
+ Hello! You can close this notification here: + +
+
+ `; +}; +``` + +### Global + +```js story +export const placementGlobal = () => { + const placementModeGlobalConfig = { placementMode: 'global' }; + return html` + + +
+ Hello! You can close this notification here: + +
+
+ `; +}; +``` + +## 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' | | | | diff --git a/docs/fundamentals/systems/overlays/positioning.md b/docs/fundamentals/systems/overlays/_edge-cases.md similarity index 77% rename from docs/fundamentals/systems/overlays/positioning.md rename to docs/fundamentals/systems/overlays/_edge-cases.md index 5f28736c6..e6046833c 100644 --- a/docs/fundamentals/systems/overlays/positioning.md +++ b/docs/fundamentals/systems/overlays/_edge-cases.md @@ -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 `` 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``; -``` - -## 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``; -``` - ## 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. diff --git a/docs/fundamentals/systems/overlays/assets/applyDemoOverlayStyles.mjs b/docs/fundamentals/systems/overlays/assets/applyDemoOverlayStyles.mjs index b3b70f466..f5203eb44 100644 --- a/docs/fundamentals/systems/overlays/assets/applyDemoOverlayStyles.mjs +++ b/docs/fundamentals/systems/overlays/assets/applyDemoOverlayStyles.mjs @@ -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); }; diff --git a/docs/fundamentals/systems/overlays/assets/demo-overlay-system.mjs b/docs/fundamentals/systems/overlays/assets/demo-el-using-overlaymixin.mjs similarity index 93% rename from docs/fundamentals/systems/overlays/assets/demo-overlay-system.mjs rename to docs/fundamentals/systems/overlays/assets/demo-el-using-overlaymixin.mjs index 66e6d8feb..1fe27566a 100644 --- a/docs/fundamentals/systems/overlays/assets/demo-overlay-system.mjs +++ b/docs/fundamentals/systems/overlays/assets/demo-el-using-overlaymixin.mjs @@ -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) { - -
popup is ${this.opened ? 'opened' : 'closed'}
`; } } -customElements.define('demo-overlay-system', DemoOverlaySystem); +customElements.define('demo-el-using-overlaymixin', DemoElUsingOverlayMixin); class DemoOverlay extends OverlayMixin(LitElement) { static get styles() { diff --git a/docs/fundamentals/systems/overlays/assets/demo-overlay-backdrop.mjs b/docs/fundamentals/systems/overlays/assets/demo-overlay-backdrop.mjs index 62b05c269..36fe51b90 100644 --- a/docs/fundamentals/systems/overlays/assets/demo-overlay-backdrop.mjs +++ b/docs/fundamentals/systems/overlays/assets/demo-overlay-backdrop.mjs @@ -14,6 +14,7 @@ class DemoOverlayBackdrop extends LitElement { height: 100%; background-color: grey; opacity: 0.3; + position: fixed; } :host(.local-overlays__backdrop--visible) { diff --git a/docs/fundamentals/systems/overlays/assets/demo-overlay-positioning.mjs b/docs/fundamentals/systems/overlays/assets/demo-overlay-positioning.mjs index 84d917750..ee796d591 100644 --- a/docs/fundamentals/systems/overlays/assets/demo-overlay-positioning.mjs +++ b/docs/fundamentals/systems/overlays/assets/demo-overlay-positioning.mjs @@ -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; } diff --git a/docs/fundamentals/systems/overlays/configuration.md b/docs/fundamentals/systems/overlays/configuration.md index 157a55d9d..ff11034fc 100644 --- a/docs/fundamentals/systems/overlays/configuration.md +++ b/docs/fundamentals/systems/overlays/configuration.md @@ -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 ``. +or in the `overlays` container at the bottom of the ``. ### Local @@ -28,7 +28,7 @@ export const placementLocal = () => { border: 1px solid black; } - +
Hello! You can close this notification here: @@ -39,7 +39,7 @@ export const placementLocal = () => { ⨯
-
+ `; }; ``` @@ -50,7 +50,7 @@ export const placementLocal = () => { export const placementGlobal = () => { const placementModeGlobalConfig = { placementMode: 'global' }; return html` - +
Hello! You can close this notification here: @@ -61,7 +61,7 @@ export const placementGlobal = () => { ⨯
-
+ `; }; ``` @@ -91,12 +91,12 @@ export const isTooltip = () => { const tooltipConfig = { placementMode: 'local', isTooltip: true, handlesAccessibility: true }; return html` - +
Hello!
-
+ `; }; ``` @@ -114,7 +114,7 @@ You use the feature on any type of overlay. export const trapsKeyboardFocus = () => { const trapsKeyboardFocusConfig = { trapsKeyboardFocus: true }; return html` - +
@@ -127,7 +127,7 @@ export const trapsKeyboardFocus = () => { ⨯
-
+ `; }; ``` @@ -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` - +
Hello! You can close this notification here: @@ -151,7 +151,7 @@ export const hidesOnEsc = () => { ⨯
-
+ `; }; ``` @@ -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` - +
Hello! You can close this notification here: @@ -175,7 +175,7 @@ export const hidesOnOutsideEsc = () => { ⨯
-
+ `; }; ``` @@ -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` - +
@@ -200,7 +200,7 @@ export const hidesOnOutsideClick = () => { ⨯
-
+ `; }; ``` @@ -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` - +
Hello! You can close this notification here: @@ -226,7 +229,8 @@ export const elementToFocusAfterHide = () => { ⨯
-
+ + ${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` - +
Hello! You can close this notification here: @@ -261,7 +265,7 @@ export const hasBackdrop = () => { ⨯
-
+ `; }; ``` @@ -274,24 +278,10 @@ Boolean property. When true, will block other overlays. export const isBlocking = () => { const isBlockingConfig = { hasBackdrop: true, isBlocking: true }; return html` - - -
-
- - -
- Hello! You can close this notification here: - -
-
-
- Hello! You can close this notification here: + + +
+ This overlay gets closed when overlay B gets opened
- +
+ + +
+ Overlay A is hidden... now close me and see overlay A again. + +
+
`; }; ``` @@ -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` - +
Hello! You can close this notification here: @@ -326,7 +328,7 @@ export const preventsScroll = () => { ⨯
-
+ `; }; ``` @@ -354,7 +356,7 @@ export const viewportConfig = () => { viewportConfig: { placement: 'bottom-left' }, }; return html` - +
Hello! You can close this notification here: @@ -365,7 +367,7 @@ export const viewportConfig = () => { ⨯
-
+ `; }; ``` @@ -434,7 +436,7 @@ export const popperConfig = () => { border: 1px solid black; } - +
Hello! You can close this notification here: @@ -445,7 +447,7 @@ export const popperConfig = () => { ⨯
-
+ `; }; ``` diff --git a/docs/fundamentals/systems/overlays/form-integration.md b/docs/fundamentals/systems/overlays/form-integration.md deleted file mode 100644 index 24d31107c..000000000 --- a/docs/fundamentals/systems/overlays/form-integration.md +++ /dev/null @@ -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` - - -
-

Select Rick example

- - Red - Hotpink - Teal - -

- You can close this dialog here: - -

-
-
-`; -``` - -## Input Datepicker - -Opening an Input Datepicker inside a dialog. - -```js preview-story -export const inputDatepicker = () => html` - - -
-

Input Datepicker example

- -

- You can close this dialog here: - -

-
-
-`; -``` diff --git a/docs/fundamentals/systems/overlays/use-cases.md b/docs/fundamentals/systems/overlays/use-cases.md index e5f92345f..ea7e9515b 100644 --- a/docs/fundamentals/systems/overlays/use-cases.md +++ b/docs/fundamentals/systems/overlays/use-cases.md @@ -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` - +
Hello! You can close this notification here: @@ -33,7 +33,7 @@ export const main = () => html` ⨯
-
+ `; ``` @@ -132,7 +132,7 @@ render() { or declaratively in your template with the `.config` property ```html - +
Hello! You can close this notification here: @@ -142,7 +142,7 @@ or declaratively in your template with the `.config` property ⨯
-
+ ``` ### Backdrop @@ -155,7 +155,7 @@ The easiest way is declarative. This can be achieved by adding a `
@@ -166,7 +166,7 @@ export const backdrop = () => { ⨯
- + `; }; ``` @@ -179,7 +179,7 @@ export const backdropImperative = () => { const backdropNode = document.createElement('demo-overlay-backdrop'); const responsiveModalDialogConfig = { ...withModalDialogConfig(), backdropNode }; return html` - +
Hello! You can close this notification here: @@ -189,7 +189,7 @@ export const backdropImperative = () => { ⨯
-
+ `; }; ``` @@ -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` - +
@@ -245,7 +245,7 @@ export const backdropAnimation = () => { ⨯
-
+ `; }; ``` @@ -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` - { if (window.innerWidth >= 600) { @@ -283,7 +283,7 @@ export const responsiveSwitching = () => { ⨯
-
+ `; }; ``` @@ -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 = () => {
- +
Hello! You can close this notification here: @@ -375,7 +375,7 @@ export const responsiveSwitching2 = () => { ⨯
-
+ `; }; ``` @@ -401,7 +401,7 @@ export const openedState = () => { } return html` appState.opened: ${appState.opened} - { ⨯
- + `; }; ``` @@ -449,7 +449,7 @@ export const interceptingOpenClose = () => { > ${blockOverlay} - { Hello! You can close this notification here:
- + `; }; ``` @@ -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` - +
Hello! You can close this notification here: @@ -555,8 +555,11 @@ export const overlayManager = () => { Click me to open another overlay which is blocking
-
- + +
Hello! You can close this notification here:
-
+ `; }; ``` @@ -580,7 +583,7 @@ Here is the example below export const localBackdrop = () => { const localBackdropConfig = { placementMode: 'local' }; return html` - +
@@ -591,7 +594,7 @@ export const localBackdrop = () => { ⨯
-
+ `; }; ``` @@ -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` - +
open nested overlay: - +
Nested content
-
+
-
+ `; }; ``` diff --git a/packages/ui/components/dialog/test/lion-dialog.test.js b/packages/ui/components/dialog/test/lion-dialog.test.js index 82e633f40..9f0d145ab 100644 --- a/packages/ui/components/dialog/test/lion-dialog.test.js +++ b/packages/ui/components/dialog/test/lion-dialog.test.js @@ -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 = ''; } diff --git a/packages/ui/components/input-datepicker/src/localizeNamespaceLoader.js b/packages/ui/components/input-datepicker/src/localizeNamespaceLoader.js index b8c4556e5..2e2341a0a 100644 --- a/packages/ui/components/input-datepicker/src/localizeNamespaceLoader.js +++ b/packages/ui/components/input-datepicker/src/localizeNamespaceLoader.js @@ -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': diff --git a/packages/ui/components/input-datepicker/test/lion-input-datepicker.test.js b/packages/ui/components/input-datepicker/test/lion-input-datepicker.test.js index 969641424..56060448f 100644 --- a/packages/ui/components/input-datepicker/test/lion-input-datepicker.test.js +++ b/packages/ui/components/input-datepicker/test/lion-input-datepicker.test.js @@ -685,9 +685,7 @@ describe('', () => { 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; }); diff --git a/packages/ui/components/overlays/src/OverlayController.js b/packages/ui/components/overlays/src/OverlayController.js index 4012cc9fa..f7d4e2476 100644 --- a/packages/ui/components/overlays/src/OverlayController.js +++ b/packages/ui/components/overlays/src/OverlayController.js @@ -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': diff --git a/packages/ui/components/overlays/src/OverlayMixin.js b/packages/ui/components/overlays/src/OverlayMixin.js index 9e9499873..a4b4d9ba8 100644 --- a/packages/ui/components/overlays/src/OverlayMixin.js +++ b/packages/ui/components/overlays/src/OverlayMixin.js @@ -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 || []), ], }, }); diff --git a/packages/ui/components/overlays/src/OverlaysManager.js b/packages/ui/components/overlays/src/OverlaysManager.js index 9fdf860a8..bbeb4dfbd 100644 --- a/packages/ui/components/overlays/src/OverlaysManager.js +++ b/packages/ui/components/overlays/src/OverlaysManager.js @@ -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'); - if (isIOS || isMacSafari) { - document.body.classList.remove('global-overlays-scroll-lock-ios-fix'); - } - if (isIOS) { - document.documentElement.classList.remove('global-overlays-scroll-lock-ios-fix'); - } + document.body.classList.remove('overlays-scroll-lock'); + if (isIOS || isMacSafari) { + document.body.classList.remove('overlays-scroll-lock-ios-fix'); + } + if (isIOS) { + document.documentElement.classList.remove('overlays-scroll-lock-ios-fix'); } } diff --git a/packages/ui/components/overlays/src/globalOverlaysStyle.js b/packages/ui/components/overlays/src/globalOverlaysStyle.js deleted file mode 100644 index 59969afea..000000000 --- a/packages/ui/components/overlays/src/globalOverlaysStyle.js +++ /dev/null @@ -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; - } - } -`; diff --git a/packages/ui/components/overlays/src/overlayDocumentStyle.js b/packages/ui/components/overlays/src/overlayDocumentStyle.js new file mode 100644 index 000000000..5dbd32488 --- /dev/null +++ b/packages/ui/components/overlays/src/overlayDocumentStyle.js @@ -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; + } +`; diff --git a/packages/ui/components/overlays/src/overlayShadowDomStyle.js b/packages/ui/components/overlays/src/overlayShadowDomStyle.js new file mode 100644 index 000000000..1f9086f22 --- /dev/null +++ b/packages/ui/components/overlays/src/overlayShadowDomStyle.js @@ -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; + } +`; diff --git a/packages/ui/components/overlays/test-suites/OverlayMixin.suite.js b/packages/ui/components/overlays/test-suites/OverlayMixin.suite.js index 1f4b960e3..8b5d38436 100644 --- a/packages/ui/components/overlays/test-suites/OverlayMixin.suite.js +++ b/packages/ui/components/overlays/test-suites/OverlayMixin.suite.js @@ -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) { diff --git a/packages/ui/components/overlays/test/OverlayController.test.js b/packages/ui/components/overlays/test/OverlayController.test.js index ae46b899e..78bf22da4 100644 --- a/packages/ui/components/overlays/test/OverlayController.test.js +++ b/packages/ui/components/overlays/test/OverlayController.test.js @@ -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('
')); const overlay = new OverlayController({ ...withLocalTestConfig(), contentNode, @@ -236,7 +234,7 @@ describe('OverlayController', () => { // The total dom structure created... expect(el).shadowDom.to.equal(` - +
@@ -263,7 +261,7 @@ describe('OverlayController', () => { // The total dom structure created... expect(el).lightDom.to.equal(` - +
non projected
@@ -304,7 +302,7 @@ describe('OverlayController', () => { // The total dom structure created... expect(el).shadowDom.to.equal(` - +
@@ -347,7 +345,7 @@ describe('OverlayController', () => { // The total dom structure created... expect(el).shadowDom.to.equal(` - +
@@ -414,8 +412,8 @@ describe('OverlayController', () => { // The total dom structure created... expect(el).shadowDom.to.equal( ` - -
+ +
@@ -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 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; }); }); diff --git a/packages/ui/components/overlays/test/OverlaysManager.test.js b/packages/ui/components/overlays/test/OverlaysManager.test.js index 401442188..e393865d1 100644 --- a/packages/ui/components/overlays/test/OverlaysManager.test.js +++ b/packages/ui/components/overlays/test/OverlaysManager.test.js @@ -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', ); }); }); diff --git a/packages/ui/components/overlays/test/global-positioning.test.js b/packages/ui/components/overlays/test/global-positioning.test.js index 7153ff7c3..95fec458d 100644 --- a/packages/ui/components/overlays/test/global-positioning.test.js +++ b/packages/ui/components/overlays/test/global-positioning.test.js @@ -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; }); diff --git a/packages/ui/components/overlays/types/OverlayConfig.ts b/packages/ui/components/overlays/types/OverlayConfig.ts index 4390a5d18..16ad12452 100644 --- a/packages/ui/components/overlays/types/OverlayConfig.ts +++ b/packages/ui/components/overlays/types/OverlayConfig.ts @@ -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; + /** 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; - /** 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; diff --git a/packages/ui/docs/fundamentals/systems/overlays/assets/applyDemoOverlayStyles.js b/packages/ui/docs/fundamentals/systems/overlays/assets/applyDemoOverlayStyles.js index fb9b4e7f9..93a79b2c1 100644 --- a/packages/ui/docs/fundamentals/systems/overlays/assets/applyDemoOverlayStyles.js +++ b/packages/ui/docs/fundamentals/systems/overlays/assets/applyDemoOverlayStyles.js @@ -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); }; diff --git a/packages/ui/docs/fundamentals/systems/overlays/assets/demo-overlay-system.js b/packages/ui/docs/fundamentals/systems/overlays/assets/demo-el-using-overlaymixin.js similarity index 76% rename from packages/ui/docs/fundamentals/systems/overlays/assets/demo-overlay-system.js rename to packages/ui/docs/fundamentals/systems/overlays/assets/demo-el-using-overlaymixin.js index 1a7026fe7..8c0e6c4fe 100644 --- a/packages/ui/docs/fundamentals/systems/overlays/assets/demo-overlay-system.js +++ b/packages/ui/docs/fundamentals/systems/overlays/assets/demo-el-using-overlaymixin.js @@ -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) {
-
popup is ${this.opened ? 'opened' : 'closed'}
`; } } -customElements.define('demo-overlay-system', DemoOverlaySystem); +customElements.define('demo-el-using-overlaymixin', DemoElUsingOverlayMixin); diff --git a/packages/ui/docs/fundamentals/systems/overlays/assets/demo-overlay-backdrop.js b/packages/ui/docs/fundamentals/systems/overlays/assets/demo-overlay-backdrop.js index 54059a83b..47112fcef 100644 --- a/packages/ui/docs/fundamentals/systems/overlays/assets/demo-overlay-backdrop.js +++ b/packages/ui/docs/fundamentals/systems/overlays/assets/demo-overlay-backdrop.js @@ -13,6 +13,7 @@ class DemoOverlayBackdrop extends LitElement { height: 100%; background-color: grey; opacity: 0.3; + position: fixed; } :host(.local-overlays__backdrop--visible) { diff --git a/packages/ui/exports/overlays.js b/packages/ui/exports/overlays.js index ed03b0bd9..1b78c59af 100644 --- a/packages/ui/exports/overlays.js +++ b/packages/ui/exports/overlays.js @@ -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';