-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(esl-modal): beta version of
esl-modal
created and available fo…
- Loading branch information
Showing
9 changed files
with
246 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -25,3 +25,5 @@ | |
@import "./esl-animate/core.less"; | ||
|
||
@import "./esl-share/core.less"; | ||
|
||
@import "./esl-modal/core.less"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
@import "./core/esl-modal.less"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |