Skip to content

Commit

Permalink
feat(esl-modal): beta version of esl-modal created and available fo…
Browse files Browse the repository at this point in the history
…r testing (#1376)

Closes: #1376
  • Loading branch information
NastaLeo committed Jun 29, 2023
1 parent d7e859e commit 70f6384
Show file tree
Hide file tree
Showing 9 changed files with 246 additions and 0 deletions.
3 changes: 3 additions & 0 deletions pages/src/localdev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
ESLAnimate,
ESLAnimateMixin,
ESLShare,
ESLModal,
ESLRelatedTarget
} from '../../src/modules/all';

Expand Down Expand Up @@ -104,6 +105,8 @@ ESLTooltip.register();
ESLAnimate.register();
ESLAnimateMixin.register();

ESLModal.register();

ESLShare.config(() => fetch('/assets/share/config.json').then((response) => response.json()));
ESLShare.register();

Expand Down
2 changes: 2 additions & 0 deletions src/modules/all.less
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,5 @@
@import "./esl-animate/core.less";

@import "./esl-share/core.less";

@import "./esl-modal/core.less";
3 changes: 3 additions & 0 deletions src/modules/all.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ export * from './esl-tooltip/core';
// Animate
export * from './esl-animate/core';

// Modal
export * from './esl-modal/core';

// Related Target Mixin
export * from './esl-related-target/core';

Expand Down
30 changes: 30 additions & 0 deletions src/modules/esl-modal/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# [ESL](../../../) Modal

Version: *1.0.0-beta*.

Authors: *Anastasiya Lesun*.

<a name="intro"></a>

**ESLModal** - a custom element based on `ESLToggleable` instance.

`ESLModal` opens in overlay on top of the main content when `esl:show` DOM event is dispatched on the appropriate modal item.
By default, modal window before its opening is moved to the `document.body` (determined by `body-inject` attribute) and blocks all other workflows on the main page until modal is closed (supports 'none' | 'native' | 'pseudo' locks using `scroll-lock-strategy` attribute).
Modal opening can be taken up together with backdrop appearance (depends on `no-backdrop` attribute).

### ESLModal Attributes | Properties:
- `no-backdrop` (boolean) - disable modal backdrop
- `body-inject` (boolean) - provide element movement to body before its opening
- `scroll-lock-strategy` ('none' | 'native' | 'pseudo') - define scroll lock type

### Example
```html
<body>
<main>
...
<esl-trigger target="::find(#modal-1)"></esl-trigger>
<esl-modal id="modal-1" body-inject></esl-modal>
...
</main>
</body>
```
1 change: 1 addition & 0 deletions src/modules/esl-modal/core.less
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@import "./core/esl-modal.less";
3 changes: 3 additions & 0 deletions src/modules/esl-modal/core.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export type {ESLModalTagShape} from './core/esl-modal.shape';

export * from './core/esl-modal';
64 changes: 64 additions & 0 deletions src/modules/esl-modal/core/esl-modal.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
esl-modal {
display: none;
}

.esl-modal {
position: fixed;
z-index: 1000000;
left: 0;
top: 0;
width: 100vw;
height: var(--100vh, 100vh);
display: flex;
flex-direction: column;
align-items: center;
overflow-y: scroll;

&:not(.open) {
display: none;
}

&::before, &::after {
content: '';
flex: 1 0 30px;
}

.esl-modal-container {
flex: 0 0 auto;
position: relative;
padding: 50px;
max-width: 100%;
width: 80%;
background-color: #fff;
overflow: auto;
}

.close-btn {
position: absolute;
right: 15px;
top: 15px;
width: 20px;
height: 20px;
font-size: 19px;
line-height: 19px;

&::before {
content: '\2715';
}
}
}

.esl-modal-backdrop {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: -1;
transition: all .3s ease-in;

&.active {
background: rgba(0, 0, 0, .4);
z-index: 999999;
}
}
24 changes: 24 additions & 0 deletions src/modules/esl-modal/core/esl-modal.shape.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type {ESLToggleableTagShape} from '../../esl-toggleable/core/esl-toggleable.shape';
import type {ESLModal, ScrollLockStrategies} from './esl-modal';

/**
* Tag declaration interface of {@link ESLModal} element
* Used for TSX declaration
*/
export interface ESLModalTagShape extends ESLToggleableTagShape<ESLModal> {
/** Disable modal backdrop */
'no-backdrop'?: boolean;
/** Provides modal movement to body before its opening */
'body-inject'?: boolean;
/** Defines a scroll lock strategy when the modal is open (default 'pseudo') */
'scroll-lock-strategy'?: ScrollLockStrategies;
}

declare global {
namespace JSX {
export interface IntrinsicElements {
/** {@link ESLModal} custom tag */
'esl-modal': ESLModalTagShape;
}
}
}
116 changes: 116 additions & 0 deletions src/modules/esl-modal/core/esl-modal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import {ExportNs} from '../../esl-utils/environment/export-ns';
import {ESLToggleable} from '../../esl-toggleable/core/esl-toggleable';
import {prop, boolAttr, attr, memoize, listen} from '../../esl-utils/decorators';
import {hasAttr, setAttr} from '../../esl-utils/dom/attr';
import {getKeyboardFocusableElements, handleFocusChain} from '../../esl-utils/dom/focus';
import {lockScroll, unlockScroll} from '../../esl-utils/dom/scroll/utils';
import {TAB} from '../../esl-utils/dom/keys';

import type {ScrollLockOptions} from '../../esl-utils/dom/scroll/utils';
import type {ESLToggleableActionParams} from '../../esl-toggleable/core/esl-toggleable';

export type ScrollLockStrategies = ScrollLockOptions['strategy'];

export interface ModalActionParams extends ESLToggleableActionParams { }

@ExportNs('Modal')
export class ESLModal extends ESLToggleable {
public static override is = 'esl-modal';

@attr({defaultValue: '[data-modal-close]'})
public override closeTrigger: string;

/** Define option to lock scroll {@see ScrollLockOptions} */
@attr({defaultValue: 'pseudo'})
public scrollLockStrategy: ScrollLockStrategies;

@boolAttr() public noBackdrop: boolean;
@boolAttr() public bodyInject: boolean;

@prop(true) public override closeOnEsc: boolean;
@prop(true) public override closeOnOutsideAction: boolean;

@memoize()
protected static get $backdrop(): any {
const $backdrop = document.createElement('esl-modal-backdrop');
$backdrop.classList.add('esl-modal-backdrop');
return $backdrop;
}

public override connectedCallback(): void {
super.connectedCallback();
if (!hasAttr(this, 'role')) setAttr(this, 'role', 'dialog');
if (!hasAttr(this, 'tabindex')) setAttr(this, 'tabIndex', '-1');
}

public override onShow(params: ModalActionParams): void {
this.bodyInject && this.inject();
this.activator = params.activator;
this.showBackdrop();
super.onShow(params);
this.focus();
lockScroll(document.documentElement, this.lockOptions);
}

public override onHide(params: ModalActionParams): void {
unlockScroll(document.documentElement, this.lockOptions);
super.onHide(params);
this.hideBackdrop();
this.activator?.focus();
this.bodyInject && this.extract();
}

protected inject(): void {
if (this.parentNode === document.body) return;
document.body.appendChild(this);
}

protected extract(): void {
if (this.parentNode !== document.body) return;
document.body.removeChild(this);
}


protected showBackdrop(): void {
if (this.noBackdrop) return;
if (!document.body.contains(ESLModal.$backdrop)) document.body.appendChild(ESLModal.$backdrop);
ESLModal.$backdrop.classList.add('active');
}

protected hideBackdrop(): void {
if (this.noBackdrop) return;
ESLModal.$backdrop.classList.remove('active');
}

public get lockOptions(): ScrollLockOptions {
return {
strategy: this.scrollLockStrategy,
initiator: this
};
}

public get $boundaryFocusable(): {first: HTMLElement, last: HTMLElement} {
const $focusableEls = getKeyboardFocusableElements(this) as HTMLElement[];
return {first: $focusableEls[0], last: $focusableEls[$focusableEls.length - 1]};
}

@listen({inherit: true})
protected override _onKeyboardEvent(e: KeyboardEvent): void {
super._onKeyboardEvent(e);
if (e.key === TAB) this._onTabKey(e);
}

protected _onTabKey(e: KeyboardEvent): boolean | undefined {
const {first, last} = this.$boundaryFocusable;
return handleFocusChain(e, first, last);
}
}

declare global {
export interface ESLLibrary {
Modal: typeof ESLModal;
}
export interface HTMLElementTagNameMap {
'esl-modal': ESLModal;
}
}

0 comments on commit 70f6384

Please sign in to comment.