Skip to content

Commit a8c2d07

Browse files
committed
feat: implement <Modal>
1 parent 472b240 commit a8c2d07

File tree

2 files changed

+130
-3
lines changed

2 files changed

+130
-3
lines changed

src/Modal/index.ts

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import {Component} from 'react';
2+
import {render} from 'react-universal-interface';
3+
import {h, isClient, on, off, noop} from '../util';
4+
import {Overlay} from '../Overlay';
5+
6+
let cnt = 0;
7+
let id = 0;
8+
9+
const ESC = 27;
10+
11+
export interface IModalProps {
12+
onElement?: (el: HTMLDivElement) => void;
13+
onEsc?: (event) => void;
14+
}
15+
16+
export interface IModalState {
17+
}
18+
19+
class Modal extends Component<IModalProps, IModalState> {
20+
id: number;
21+
el: HTMLElement = null;
22+
activeEl: Element; // Previous active element;
23+
24+
constructor (props, context) {
25+
super(props, context);
26+
27+
cnt++;
28+
this.id = id++;
29+
30+
this.state = {
31+
bindTitle: {
32+
id: 'dialog-title-' + this.id
33+
},
34+
bindDescr: {
35+
id: 'dialog-descr-' + this.id
36+
}
37+
};
38+
39+
this.activeEl = isClient ? document.activeElement : null;
40+
}
41+
42+
componentDidMount () {
43+
on(document, 'keydown', this.onKey);
44+
45+
setTimeout(() => {
46+
const firstFocusableElement = this.el.querySelector('button, [href], input, select, textarea, [tabindex]:not(tabindex="-1"]') as HTMLElement;
47+
48+
if (firstFocusableElement && firstFocusableElement.focus) {
49+
firstFocusableElement.focus();
50+
}
51+
});
52+
}
53+
54+
componentWillUnmount () {
55+
cnt--;
56+
57+
off(document, 'keydown', this.onKey);
58+
59+
const siblings = Array.from(document.body.children);
60+
61+
for (let i = 0; i < siblings.length; i++) {
62+
const sibling = siblings[i] as HTMLElement;
63+
64+
if (sibling === this.el) {
65+
continue;
66+
}
67+
68+
if ((sibling as any).__modal_lock !== this) {
69+
continue;
70+
}
71+
72+
delete (sibling as any).__modal_lock;
73+
(sibling as any).inert = false;
74+
sibling.style.removeProperty('pointer-events');
75+
sibling.removeAttribute('aria-hidden');
76+
}
77+
78+
// Focus previously active element.
79+
if (this.activeEl && (this.activeEl as any).focus) {
80+
(this.activeEl as any).focus();
81+
}
82+
}
83+
84+
onElement = (el) => {
85+
this.el = el;
86+
87+
el.setAttribute('role', 'dialog');
88+
el.classList.add('dialog');
89+
90+
el.setAttribute('aria-labelledby', 'dialog-title-' + this.id);
91+
el.setAttribute('aria-describedby', 'dialog-descr-' + this.id);
92+
93+
const siblings = Array.from(document.body.children);
94+
95+
for (let i = 0; i < siblings.length; i++) {
96+
const sibling = siblings[i] as HTMLElement;
97+
98+
if (sibling === el) {
99+
continue;
100+
}
101+
102+
if ((sibling as any).__modal_lock) {
103+
continue;
104+
}
105+
106+
(sibling as any).__modal_lock = this;
107+
(sibling as any).inert = true;
108+
sibling.style.setProperty('pointer-events', 'none');
109+
sibling.setAttribute('aria-hidden', 'true');
110+
}
111+
112+
(this.props.onElement || noop)(el);
113+
};
114+
115+
onKey = (event) => {
116+
if (event.keyCode === ESC) {
117+
(this.props.onEsc || noop)(event);
118+
}
119+
};
120+
121+
render () {
122+
return h(Overlay, {
123+
onElement: this.onElement
124+
}, render(this.props, this.state));
125+
}
126+
}

src/Overlay/index.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import {Component} from 'react';
22
import {Portal} from '../Portal';
3-
import {h} from '../util';
3+
import {h, noop} from '../util';
44

55
export interface IOverlayProps {
66
color?: string;
77
time?: number;
8+
onElement?: (div: HTMLElement) => void;
89
}
910

1011
export interface IOverlayState {
@@ -19,8 +20,6 @@ export class Overlay extends Component<IOverlayProps, IOverlayState> {
1920
onElement = (el) => {
2021
const {style} = el;
2122

22-
el.setAttribute('role', 'modal');
23-
2423
style.zIndex = 2147483647; // Max z-index.
2524
style.position = 'fixed';
2625
style.width = '100%';
@@ -41,6 +40,8 @@ export class Overlay extends Component<IOverlayProps, IOverlayState> {
4140
setTimeout(() => {
4241
style.opacity = 1;
4342
}, 35);
43+
44+
(this.props.onElement || noop)(el);
4445
};
4546

4647
render () {

0 commit comments

Comments
 (0)