@alfalab/core-components-base-modal 5.2.0 → 5.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/Component.js CHANGED
@@ -23,7 +23,7 @@ var FocusLock__default = /*#__PURE__*/_interopDefaultCompat(FocusLock);
23
23
  var mergeRefs__default = /*#__PURE__*/_interopDefaultCompat(mergeRefs);
24
24
  var cn__default = /*#__PURE__*/_interopDefaultCompat(cn);
25
25
 
26
- var styles = {"component":"base-modal__component_1n9jh","wrapper":"base-modal__wrapper_1n9jh","content":"base-modal__content_1n9jh","hidden":"base-modal__hidden_1n9jh","backdrop":"base-modal__backdrop_1n9jh","appear":"base-modal__appear_1n9jh","enter":"base-modal__enter_1n9jh","appearActive":"base-modal__appearActive_1n9jh","enterActive":"base-modal__enterActive_1n9jh","exit":"base-modal__exit_1n9jh","exitActive":"base-modal__exitActive_1n9jh","exitDone":"base-modal__exitDone_1n9jh"};
26
+ var styles = {"component":"base-modal__component_mx6kj","wrapper":"base-modal__wrapper_mx6kj","content":"base-modal__content_mx6kj","hidden":"base-modal__hidden_mx6kj","backdrop":"base-modal__backdrop_mx6kj","appear":"base-modal__appear_mx6kj","enter":"base-modal__enter_mx6kj","appearActive":"base-modal__appearActive_mx6kj","enterActive":"base-modal__enterActive_mx6kj","exit":"base-modal__exit_mx6kj","exitActive":"base-modal__exitActive_mx6kj","exitDone":"base-modal__exitDone_mx6kj"};
27
27
  require('./index.css')
28
28
 
29
29
  // eslint-disable-next-line @typescript-eslint/no-redeclare
package/esm/Component.js CHANGED
@@ -12,7 +12,7 @@ import { isScrolledToTop, isScrolledToBottom, handleContainer, restoreContainerS
12
12
  import './matches-polyfill.js';
13
13
  import '@alfalab/core-components-global-store/esm';
14
14
 
15
- var styles = {"component":"base-modal__component_1n9jh","wrapper":"base-modal__wrapper_1n9jh","content":"base-modal__content_1n9jh","hidden":"base-modal__hidden_1n9jh","backdrop":"base-modal__backdrop_1n9jh","appear":"base-modal__appear_1n9jh","enter":"base-modal__enter_1n9jh","appearActive":"base-modal__appearActive_1n9jh","enterActive":"base-modal__enterActive_1n9jh","exit":"base-modal__exit_1n9jh","exitActive":"base-modal__exitActive_1n9jh","exitDone":"base-modal__exitDone_1n9jh"};
15
+ var styles = {"component":"base-modal__component_mx6kj","wrapper":"base-modal__wrapper_mx6kj","content":"base-modal__content_mx6kj","hidden":"base-modal__hidden_mx6kj","backdrop":"base-modal__backdrop_mx6kj","appear":"base-modal__appear_mx6kj","enter":"base-modal__enter_mx6kj","appearActive":"base-modal__appearActive_mx6kj","enterActive":"base-modal__enterActive_mx6kj","exit":"base-modal__exit_mx6kj","exitActive":"base-modal__exitActive_mx6kj","exitDone":"base-modal__exitDone_mx6kj"};
16
16
  require('./index.css')
17
17
 
18
18
  // eslint-disable-next-line @typescript-eslint/no-redeclare
package/esm/index.css CHANGED
@@ -1,4 +1,4 @@
1
- /* hash: d8454 */
1
+ /* hash: vww7l */
2
2
  :root {
3
3
  } /* deprecated */ :root {
4
4
  --color-light-bg-primary: #fff; /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */
@@ -15,13 +15,13 @@
15
15
  } :root {
16
16
  } :root {
17
17
  } :root {
18
- } .base-modal__component_1n9jh {
18
+ } .base-modal__component_mx6kj {
19
19
  position: relative;
20
20
  box-sizing: border-box;
21
21
  background: var(--color-light-bg-primary);
22
22
  margin: auto;
23
23
  flex-shrink: 0;
24
- } .base-modal__wrapper_1n9jh {
24
+ } .base-modal__wrapper_mx6kj {
25
25
  position: fixed;
26
26
  top: 0;
27
27
  left: 0;
@@ -33,27 +33,27 @@
33
33
  flex-direction: column;
34
34
  align-items: center;
35
35
  outline: 0;
36
- } .base-modal__content_1n9jh {
36
+ } .base-modal__content_mx6kj {
37
37
  width: 100%;
38
38
  height: 100%;
39
39
  display: flex;
40
40
  flex-direction: column;
41
41
  flex: 1;
42
- } .base-modal__hidden_1n9jh {
42
+ } .base-modal__hidden_mx6kj {
43
43
  display: none;
44
- } .base-modal__backdrop_1n9jh {
44
+ } .base-modal__backdrop_mx6kj {
45
45
  z-index: 0;
46
- } .base-modal__appear_1n9jh,
47
- .base-modal__enter_1n9jh {
46
+ } .base-modal__appear_mx6kj,
47
+ .base-modal__enter_mx6kj {
48
48
  opacity: 0;
49
- } .base-modal__appearActive_1n9jh,
50
- .base-modal__enterActive_1n9jh {
49
+ } .base-modal__appearActive_mx6kj,
50
+ .base-modal__enterActive_mx6kj {
51
51
  opacity: 1;
52
52
  transition: opacity 200ms ease-in;
53
- } .base-modal__exit_1n9jh {
53
+ } .base-modal__exit_mx6kj {
54
54
  opacity: 1;
55
- } .base-modal__exitActive_1n9jh,
56
- .base-modal__exitDone_1n9jh {
55
+ } .base-modal__exitActive_mx6kj,
56
+ .base-modal__exitDone_mx6kj {
57
57
  opacity: 0;
58
58
  transition: opacity 200ms ease-out;
59
59
  }
package/index.css CHANGED
@@ -1,4 +1,4 @@
1
- /* hash: d8454 */
1
+ /* hash: vww7l */
2
2
  :root {
3
3
  } /* deprecated */ :root {
4
4
  --color-light-bg-primary: #fff; /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */
@@ -15,13 +15,13 @@
15
15
  } :root {
16
16
  } :root {
17
17
  } :root {
18
- } .base-modal__component_1n9jh {
18
+ } .base-modal__component_mx6kj {
19
19
  position: relative;
20
20
  box-sizing: border-box;
21
21
  background: var(--color-light-bg-primary);
22
22
  margin: auto;
23
23
  flex-shrink: 0;
24
- } .base-modal__wrapper_1n9jh {
24
+ } .base-modal__wrapper_mx6kj {
25
25
  position: fixed;
26
26
  top: 0;
27
27
  left: 0;
@@ -33,27 +33,27 @@
33
33
  flex-direction: column;
34
34
  align-items: center;
35
35
  outline: 0;
36
- } .base-modal__content_1n9jh {
36
+ } .base-modal__content_mx6kj {
37
37
  width: 100%;
38
38
  height: 100%;
39
39
  display: flex;
40
40
  flex-direction: column;
41
41
  flex: 1;
42
- } .base-modal__hidden_1n9jh {
42
+ } .base-modal__hidden_mx6kj {
43
43
  display: none;
44
- } .base-modal__backdrop_1n9jh {
44
+ } .base-modal__backdrop_mx6kj {
45
45
  z-index: 0;
46
- } .base-modal__appear_1n9jh,
47
- .base-modal__enter_1n9jh {
46
+ } .base-modal__appear_mx6kj,
47
+ .base-modal__enter_mx6kj {
48
48
  opacity: 0;
49
- } .base-modal__appearActive_1n9jh,
50
- .base-modal__enterActive_1n9jh {
49
+ } .base-modal__appearActive_mx6kj,
50
+ .base-modal__enterActive_mx6kj {
51
51
  opacity: 1;
52
52
  transition: opacity 200ms ease-in;
53
- } .base-modal__exit_1n9jh {
53
+ } .base-modal__exit_mx6kj {
54
54
  opacity: 1;
55
- } .base-modal__exitActive_1n9jh,
56
- .base-modal__exitDone_1n9jh {
55
+ } .base-modal__exitActive_mx6kj,
56
+ .base-modal__exitDone_mx6kj {
57
57
  opacity: 0;
58
58
  transition: opacity 200ms ease-out;
59
59
  }
@@ -11,7 +11,7 @@ import { isScrolledToTop, isScrolledToBottom, handleContainer, restoreContainerS
11
11
  import './matches-polyfill.js';
12
12
  import '@alfalab/core-components-global-store/modern';
13
13
 
14
- const styles = {"component":"base-modal__component_1n9jh","wrapper":"base-modal__wrapper_1n9jh","content":"base-modal__content_1n9jh","hidden":"base-modal__hidden_1n9jh","backdrop":"base-modal__backdrop_1n9jh","appear":"base-modal__appear_1n9jh","enter":"base-modal__enter_1n9jh","appearActive":"base-modal__appearActive_1n9jh","enterActive":"base-modal__enterActive_1n9jh","exit":"base-modal__exit_1n9jh","exitActive":"base-modal__exitActive_1n9jh","exitDone":"base-modal__exitDone_1n9jh"};
14
+ const styles = {"component":"base-modal__component_mx6kj","wrapper":"base-modal__wrapper_mx6kj","content":"base-modal__content_mx6kj","hidden":"base-modal__hidden_mx6kj","backdrop":"base-modal__backdrop_mx6kj","appear":"base-modal__appear_mx6kj","enter":"base-modal__enter_mx6kj","appearActive":"base-modal__appearActive_mx6kj","enterActive":"base-modal__enterActive_mx6kj","exit":"base-modal__exit_mx6kj","exitActive":"base-modal__exitActive_mx6kj","exitDone":"base-modal__exitDone_mx6kj"};
15
15
  require('./index.css')
16
16
 
17
17
  /* eslint-disable jsx-a11y/no-noninteractive-element-interactions */
package/modern/index.css CHANGED
@@ -1,4 +1,4 @@
1
- /* hash: d8454 */
1
+ /* hash: vww7l */
2
2
  :root {
3
3
  } /* deprecated */ :root {
4
4
  --color-light-bg-primary: #fff; /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */ /* deprecated */
@@ -15,13 +15,13 @@
15
15
  } :root {
16
16
  } :root {
17
17
  } :root {
18
- } .base-modal__component_1n9jh {
18
+ } .base-modal__component_mx6kj {
19
19
  position: relative;
20
20
  box-sizing: border-box;
21
21
  background: var(--color-light-bg-primary);
22
22
  margin: auto;
23
23
  flex-shrink: 0;
24
- } .base-modal__wrapper_1n9jh {
24
+ } .base-modal__wrapper_mx6kj {
25
25
  position: fixed;
26
26
  top: 0;
27
27
  left: 0;
@@ -33,27 +33,27 @@
33
33
  flex-direction: column;
34
34
  align-items: center;
35
35
  outline: 0;
36
- } .base-modal__content_1n9jh {
36
+ } .base-modal__content_mx6kj {
37
37
  width: 100%;
38
38
  height: 100%;
39
39
  display: flex;
40
40
  flex-direction: column;
41
41
  flex: 1;
42
- } .base-modal__hidden_1n9jh {
42
+ } .base-modal__hidden_mx6kj {
43
43
  display: none;
44
- } .base-modal__backdrop_1n9jh {
44
+ } .base-modal__backdrop_mx6kj {
45
45
  z-index: 0;
46
- } .base-modal__appear_1n9jh,
47
- .base-modal__enter_1n9jh {
46
+ } .base-modal__appear_mx6kj,
47
+ .base-modal__enter_mx6kj {
48
48
  opacity: 0;
49
- } .base-modal__appearActive_1n9jh,
50
- .base-modal__enterActive_1n9jh {
49
+ } .base-modal__appearActive_mx6kj,
50
+ .base-modal__enterActive_mx6kj {
51
51
  opacity: 1;
52
52
  transition: opacity 200ms ease-in;
53
- } .base-modal__exit_1n9jh {
53
+ } .base-modal__exit_mx6kj {
54
54
  opacity: 1;
55
- } .base-modal__exitActive_1n9jh,
56
- .base-modal__exitDone_1n9jh {
55
+ } .base-modal__exitActive_mx6kj,
56
+ .base-modal__exitDone_mx6kj {
57
57
  opacity: 0;
58
58
  transition: opacity 200ms ease-out;
59
59
  }
package/package.json CHANGED
@@ -1,23 +1,20 @@
1
1
  {
2
2
  "name": "@alfalab/core-components-base-modal",
3
- "version": "5.2.0",
3
+ "version": "5.3.0",
4
4
  "description": "BaseModal component",
5
5
  "keywords": [],
6
6
  "license": "MIT",
7
7
  "main": "index.js",
8
8
  "module": "./esm/index.js",
9
- "scripts": {
10
- "postinstall": "node -e \"if (require('fs').existsSync('./send-stats.js')){require('./send-stats.js')} \""
11
- },
12
9
  "publishConfig": {
13
10
  "access": "public",
14
11
  "directory": "dist"
15
12
  },
16
13
  "dependencies": {
17
- "@alfalab/core-components-backdrop": "^3.0.6",
18
- "@alfalab/core-components-global-store": "^2.0.4",
19
- "@alfalab/core-components-portal": "^3.1.4",
20
- "@alfalab/core-components-stack": "^4.0.4",
14
+ "@alfalab/core-components-backdrop": "^3.1.0",
15
+ "@alfalab/core-components-global-store": "^2.1.0",
16
+ "@alfalab/core-components-portal": "^3.2.0",
17
+ "@alfalab/core-components-stack": "^4.1.0",
21
18
  "@juggle/resize-observer": "^3.3.1",
22
19
  "classnames": "^2.3.1",
23
20
  "react-focus-lock": "^2.9.3",
@@ -0,0 +1,577 @@
1
+ /* eslint-disable jsx-a11y/no-noninteractive-element-interactions */
2
+ import React, {
3
+ FC,
4
+ forwardRef,
5
+ KeyboardEvent,
6
+ MouseEvent,
7
+ MutableRefObject,
8
+ ReactNode,
9
+ Ref,
10
+ useCallback,
11
+ useEffect,
12
+ useMemo,
13
+ useRef,
14
+ useState,
15
+ } from 'react';
16
+ import FocusLock from 'react-focus-lock';
17
+ import mergeRefs from 'react-merge-refs';
18
+ import { CSSTransition } from 'react-transition-group';
19
+ import { TransitionProps } from 'react-transition-group/Transition';
20
+ import { ResizeObserver as ResizeObserverPolyfill } from '@juggle/resize-observer';
21
+ import cn from 'classnames';
22
+
23
+ import { Backdrop as DefaultBackdrop, BackdropProps } from '@alfalab/core-components-backdrop';
24
+ import { Portal, PortalProps } from '@alfalab/core-components-portal';
25
+ import { Stack, stackingOrder } from '@alfalab/core-components-stack';
26
+
27
+ import {
28
+ getScrollbarSize,
29
+ handleContainer,
30
+ hasScrollbar,
31
+ isScrolledToBottom,
32
+ isScrolledToTop,
33
+ restoreContainerStyles,
34
+ } from './utils';
35
+
36
+ import styles from './index.module.css';
37
+
38
+ // TODO Без полифила крашится FocusLock в IE11. Выпилить в будущем!!!.
39
+ import './matches-polyfill';
40
+
41
+ export type BaseModalProps = {
42
+ /**
43
+ * Контент
44
+ */
45
+ children?: ReactNode;
46
+
47
+ /**
48
+ * Компонент бэкдропа
49
+ */
50
+ Backdrop?: FC<BackdropProps>;
51
+
52
+ /**
53
+ * Свойства для Бэкдропа
54
+ */
55
+ backdropProps?: Partial<BackdropProps> & Record<string, unknown>;
56
+
57
+ /**
58
+ * Нода, компонент или функция возвращающая их
59
+ *
60
+ * Контейнер к которому будут добавляться порталы
61
+ */
62
+ container?: PortalProps['getPortalContainer'];
63
+
64
+ /**
65
+ * Отключает автоматический перевод фокуса на модалку при открытии
66
+ * @default false
67
+ */
68
+ disableAutoFocus?: boolean;
69
+
70
+ /**
71
+ * Отключает ловушку фокуса
72
+ * @default false
73
+ */
74
+ disableFocusLock?: boolean;
75
+
76
+ /**
77
+ * Отключает восстановление фокуса на предыдущем элементе после закрытия модалки
78
+ * @default false
79
+ */
80
+ disableRestoreFocus?: boolean;
81
+
82
+ /**
83
+ * Отключает вызов `callback` при нажатии Escape
84
+ * @default false
85
+ */
86
+ disableEscapeKeyDown?: boolean;
87
+
88
+ /**
89
+ * Отключает вызов `callback` при клике на бэкдроп
90
+ * @default false
91
+ */
92
+ disableBackdropClick?: boolean;
93
+
94
+ /**
95
+ * Отключает блокировку скролла при открытии модального окна
96
+ * @default false
97
+ */
98
+ disableBlockingScroll?: boolean;
99
+
100
+ /**
101
+ * Содержимое модалки всегда в DOM
102
+ * @default false
103
+ */
104
+ keepMounted?: boolean;
105
+
106
+ /**
107
+ * Управление видимостью модалки
108
+ */
109
+ open: boolean;
110
+
111
+ /**
112
+ * Дополнительный класс
113
+ */
114
+ className?: string;
115
+
116
+ /**
117
+ * Дополнительный класс
118
+ */
119
+ contentClassName?: string;
120
+
121
+ /**
122
+ * Дополнительный класс для обертки (Modal)
123
+ */
124
+ wrapperClassName?: string;
125
+
126
+ /**
127
+ * Обработчик скролла контента
128
+ */
129
+ scrollHandler?: 'wrapper' | 'content' | MutableRefObject<HTMLDivElement | null>;
130
+
131
+ /**
132
+ * Пропсы для анимации (CSSTransition)
133
+ */
134
+ transitionProps?: Partial<TransitionProps>;
135
+
136
+ /**
137
+ * Рендерить ли в контейнер через портал.
138
+ * @default true
139
+ */
140
+ usePortal?: boolean;
141
+
142
+ /**
143
+ * Обработчик события нажатия на бэкдроп
144
+ */
145
+ onBackdropClick?: (event: MouseEvent) => void;
146
+
147
+ /**
148
+ * Обработчик события нажатия на Escape
149
+ *
150
+ * Если `disableEscapeKeyDown` - false и модальное окно в фокусе
151
+ */
152
+ onEscapeKeyDown?: (event: KeyboardEvent) => void;
153
+
154
+ /**
155
+ * Обработчик закрытия
156
+ */
157
+ onClose?: (
158
+ event: MouseEvent<HTMLElement> | KeyboardEvent<HTMLElement>,
159
+ reason?: 'backdropClick' | 'escapeKeyDown' | 'closerClick',
160
+ ) => void;
161
+
162
+ /**
163
+ * Обработчик события onEntered компонента Transition
164
+ */
165
+ onMount?: () => void;
166
+
167
+ /**
168
+ * Обработчик события onExited компонента Transition
169
+ */
170
+ onUnmount?: () => void;
171
+
172
+ /**
173
+ * Идентификатор для систем автоматизированного тестирования
174
+ */
175
+ dataTestId?: string;
176
+
177
+ /**
178
+ * z-index компонента
179
+ */
180
+ zIndex?: number;
181
+
182
+ /**
183
+ * Реф, который должен быть установлен компонентной области
184
+ */
185
+ componentRef?: MutableRefObject<HTMLDivElement | null>;
186
+ };
187
+
188
+ export type BaseModalContext = {
189
+ parentRef: React.RefObject<HTMLDivElement>;
190
+ componentRef: React.RefObject<HTMLDivElement>;
191
+ hasFooter?: boolean;
192
+ hasHeader?: boolean;
193
+ hasScroll?: boolean;
194
+ headerHighlighted?: boolean;
195
+ footerHighlighted?: boolean;
196
+ headerOffset?: number;
197
+ setHeaderOffset: (offset: number) => void;
198
+ contentRef: Ref<HTMLElement>;
199
+ setHasHeader: (exists: boolean) => void;
200
+ setHasFooter: (exists: boolean) => void;
201
+ onClose: Required<BaseModalProps>['onClose'];
202
+ };
203
+
204
+ // eslint-disable-next-line @typescript-eslint/no-redeclare
205
+ export const BaseModalContext = React.createContext<BaseModalContext>({
206
+ parentRef: { current: null },
207
+ componentRef: { current: null },
208
+ hasFooter: false,
209
+ hasHeader: false,
210
+ hasScroll: false,
211
+ headerHighlighted: false,
212
+ footerHighlighted: false,
213
+ headerOffset: 0,
214
+ setHeaderOffset: () => null,
215
+ contentRef: () => null,
216
+ setHasHeader: () => null,
217
+ setHasFooter: () => null,
218
+ onClose: () => null,
219
+ });
220
+
221
+ export const BaseModal = forwardRef<HTMLDivElement, BaseModalProps>(
222
+ (
223
+ {
224
+ open,
225
+ container,
226
+ children,
227
+ scrollHandler = 'wrapper',
228
+ Backdrop = DefaultBackdrop,
229
+ backdropProps = {},
230
+ transitionProps = {},
231
+ disableBackdropClick,
232
+ disableAutoFocus = false,
233
+ disableFocusLock = false,
234
+ disableEscapeKeyDown = false,
235
+ disableRestoreFocus = false,
236
+ disableBlockingScroll = false,
237
+ keepMounted = false,
238
+ className,
239
+ contentClassName,
240
+ wrapperClassName,
241
+ onBackdropClick,
242
+ onClose,
243
+ onEscapeKeyDown,
244
+ onMount,
245
+ onUnmount,
246
+ dataTestId,
247
+ zIndex = stackingOrder.MODAL,
248
+ componentRef = null,
249
+ usePortal = true,
250
+ },
251
+ ref,
252
+ ) => {
253
+ const [exited, setExited] = useState<boolean | null>(null);
254
+ const [hasScroll, setHasScroll] = useState(false);
255
+ const [hasHeader, setHasHeader] = useState(false);
256
+ const [hasFooter, setHasFooter] = useState(false);
257
+ const [headerHighlighted, setHeaderHighlighted] = useState(false);
258
+ const [footerHighlighted, setFooterHighlighted] = useState(false);
259
+ const [headerOffset, setHeaderOffset] = useState(0);
260
+
261
+ const componentNodeRef = useRef<HTMLDivElement>(null);
262
+ const wrapperRef = useRef<HTMLDivElement>(null);
263
+ const scrollableNodeRef = useRef<HTMLDivElement | null>(null);
264
+ const contentNodeRef = useRef<HTMLDivElement | null>(null);
265
+ const restoreContainerStylesRef = useRef<null | (() => void)>(null);
266
+ const mouseDownTarget = useRef<HTMLElement>();
267
+ const resizeObserverRef = useRef<ResizeObserver>();
268
+
269
+ const checkToHasScrollBar = () => {
270
+ if (scrollableNodeRef.current) {
271
+ const scrollExists = hasScrollbar(scrollableNodeRef.current);
272
+
273
+ setFooterHighlighted(scrollExists);
274
+ setHasScroll(scrollExists);
275
+ }
276
+ };
277
+
278
+ const isExited = exited || exited === null;
279
+ const shouldRender = keepMounted || open || !isExited;
280
+
281
+ const getContainer = useCallback(
282
+ () => (container ? container() : document.body) as HTMLElement,
283
+ [container],
284
+ );
285
+
286
+ const addResizeHandle = useCallback(() => {
287
+ if (!resizeObserverRef.current) return;
288
+
289
+ if (scrollableNodeRef.current) {
290
+ resizeObserverRef.current.observe(scrollableNodeRef.current);
291
+ }
292
+ if (contentNodeRef.current) {
293
+ resizeObserverRef.current.observe(contentNodeRef.current);
294
+ }
295
+ }, []);
296
+
297
+ const removeResizeHandle = useCallback(() => resizeObserverRef.current?.disconnect(), []);
298
+
299
+ const contentRef = useCallback((node: HTMLDivElement) => {
300
+ if (node !== null) {
301
+ contentNodeRef.current = node;
302
+ if (resizeObserverRef.current) {
303
+ resizeObserverRef.current.observe(node);
304
+ }
305
+ checkToHasScrollBar();
306
+ }
307
+ }, []);
308
+
309
+ const handleScroll = useCallback(() => {
310
+ if (!scrollableNodeRef.current || !componentNodeRef.current) return;
311
+
312
+ if (hasHeader) {
313
+ setHeaderHighlighted(
314
+ !isScrolledToTop(scrollableNodeRef.current) &&
315
+ componentNodeRef.current.getBoundingClientRect().top - headerOffset <= 0,
316
+ );
317
+ }
318
+
319
+ if (hasFooter) {
320
+ setFooterHighlighted(
321
+ !isScrolledToBottom(scrollableNodeRef.current) &&
322
+ componentNodeRef.current.getBoundingClientRect().bottom >=
323
+ window.innerHeight,
324
+ );
325
+ }
326
+ }, [hasFooter, hasHeader, headerOffset]);
327
+
328
+ const handleClose = useCallback<Required<BaseModalProps>['onClose']>(
329
+ (event, reason) => {
330
+ if (onClose) {
331
+ onClose(event, reason);
332
+ }
333
+
334
+ if (reason === 'backdropClick' && onBackdropClick) {
335
+ onBackdropClick(event as MouseEvent);
336
+ }
337
+
338
+ if (reason === 'escapeKeyDown' && onEscapeKeyDown) {
339
+ onEscapeKeyDown(event as KeyboardEvent);
340
+ }
341
+
342
+ return null;
343
+ },
344
+ [onBackdropClick, onClose, onEscapeKeyDown],
345
+ );
346
+
347
+ const handleBackdropMouseDown = (event: MouseEvent<HTMLElement>) => {
348
+ let clickedOnScrollbar = false;
349
+ const clientWidth = (event.target as HTMLElement)?.clientWidth;
350
+
351
+ if (event.clientX && clientWidth) {
352
+ // Устанавливаем смещение для абсолютно спозиционированного скроллбара в OSX в 17px.
353
+ const offset = getScrollbarSize() === 0 ? 17 : 0;
354
+
355
+ clickedOnScrollbar = event.clientX + offset > clientWidth;
356
+ }
357
+
358
+ if (!disableBackdropClick && !clickedOnScrollbar) {
359
+ mouseDownTarget.current = event.target as HTMLElement;
360
+ }
361
+ };
362
+
363
+ const handleBackdropMouseUp = (event: MouseEvent<HTMLElement>) => {
364
+ if (
365
+ !disableBackdropClick &&
366
+ event.target === wrapperRef.current &&
367
+ mouseDownTarget.current === wrapperRef.current
368
+ ) {
369
+ handleClose(event, 'backdropClick');
370
+ }
371
+
372
+ mouseDownTarget.current = undefined;
373
+ };
374
+
375
+ const handleKeyDown = useCallback(
376
+ (event: KeyboardEvent<HTMLDivElement>) => {
377
+ /*
378
+ * Чтобы сохранить дефолтное поведение элементов и событий форм,
379
+ * обработчик не устанавливает event.preventDefault()
380
+ */
381
+ if (event.key !== 'Escape') {
382
+ return;
383
+ }
384
+
385
+ // Если есть обработчик escape на body
386
+ event.stopPropagation();
387
+
388
+ if (!disableEscapeKeyDown && handleClose) {
389
+ handleClose(event, 'escapeKeyDown');
390
+ }
391
+ },
392
+ [disableEscapeKeyDown, handleClose],
393
+ );
394
+
395
+ const getScrollHandler = useCallback(() => {
396
+ if (scrollHandler === 'wrapper') return wrapperRef.current;
397
+ if (scrollHandler === 'content') return componentNodeRef.current;
398
+
399
+ return scrollHandler.current || wrapperRef.current;
400
+ }, [scrollHandler]);
401
+
402
+ const handleEntered: Required<TransitionProps>['onEntered'] = useCallback(
403
+ (node, isAppearing) => {
404
+ scrollableNodeRef.current = getScrollHandler();
405
+
406
+ addResizeHandle();
407
+
408
+ if (scrollableNodeRef.current) {
409
+ scrollableNodeRef.current.addEventListener('scroll', handleScroll);
410
+ handleScroll();
411
+ }
412
+
413
+ if (transitionProps.onEntered) {
414
+ transitionProps.onEntered(node, isAppearing);
415
+ }
416
+
417
+ if (onMount) onMount();
418
+ },
419
+ [addResizeHandle, getScrollHandler, handleScroll, onMount, transitionProps],
420
+ );
421
+
422
+ const handleExited: Required<TransitionProps>['onExited'] = useCallback(
423
+ (node) => {
424
+ removeResizeHandle();
425
+
426
+ setExited(true);
427
+
428
+ if (scrollableNodeRef.current) {
429
+ scrollableNodeRef.current.removeEventListener('scroll', handleScroll);
430
+ }
431
+
432
+ if (transitionProps.onExited) {
433
+ transitionProps.onExited(node);
434
+ }
435
+
436
+ if (onUnmount) onUnmount();
437
+
438
+ if (restoreContainerStylesRef.current) {
439
+ restoreContainerStylesRef.current();
440
+ }
441
+ },
442
+ [handleScroll, onUnmount, removeResizeHandle, transitionProps],
443
+ );
444
+
445
+ useEffect(() => {
446
+ if (open && isExited) {
447
+ if (!disableBlockingScroll) {
448
+ const el = getContainer();
449
+
450
+ handleContainer(el);
451
+
452
+ restoreContainerStylesRef.current = () => {
453
+ restoreContainerStylesRef.current = null;
454
+ restoreContainerStyles(el);
455
+ };
456
+ }
457
+
458
+ setExited(false);
459
+ }
460
+ }, [getContainer, open, disableBlockingScroll, isExited]);
461
+
462
+ useEffect(() => {
463
+ const ResizeObserver = window.ResizeObserver || ResizeObserverPolyfill;
464
+
465
+ resizeObserverRef.current = new ResizeObserver(checkToHasScrollBar);
466
+
467
+ return () => {
468
+ if (restoreContainerStylesRef.current) {
469
+ restoreContainerStylesRef.current();
470
+ }
471
+
472
+ if (resizeObserverRef.current) {
473
+ resizeObserverRef.current.disconnect();
474
+ }
475
+ };
476
+ }, []);
477
+
478
+ const contextValue = useMemo<BaseModalContext>(
479
+ () => ({
480
+ parentRef: wrapperRef,
481
+ componentRef: componentNodeRef,
482
+ hasHeader,
483
+ hasFooter,
484
+ hasScroll,
485
+ headerHighlighted,
486
+ footerHighlighted,
487
+ headerOffset,
488
+ setHeaderOffset,
489
+ contentRef,
490
+ setHasHeader,
491
+ setHasFooter,
492
+ onClose: handleClose,
493
+ }),
494
+ [
495
+ contentRef,
496
+ hasHeader,
497
+ hasFooter,
498
+ hasScroll,
499
+ headerHighlighted,
500
+ footerHighlighted,
501
+ headerOffset,
502
+ setHeaderOffset,
503
+ handleClose,
504
+ ],
505
+ );
506
+
507
+ const renderContent = () => (
508
+ <Stack value={zIndex}>
509
+ {(computedZIndex) => (
510
+ <BaseModalContext.Provider value={contextValue}>
511
+ <FocusLock
512
+ autoFocus={!disableAutoFocus}
513
+ disabled={disableFocusLock || !open}
514
+ returnFocus={!disableRestoreFocus}
515
+ >
516
+ {Backdrop && (
517
+ <Backdrop
518
+ {...backdropProps}
519
+ className={cn(backdropProps.className, styles.backdrop)}
520
+ open={open}
521
+ style={{
522
+ zIndex: computedZIndex,
523
+ }}
524
+ />
525
+ )}
526
+ <div
527
+ role='dialog'
528
+ className={cn(styles.wrapper, wrapperClassName, {
529
+ [styles.hidden]: !open && isExited,
530
+ })}
531
+ ref={mergeRefs([ref, wrapperRef])}
532
+ onKeyDown={handleKeyDown}
533
+ onMouseDown={handleBackdropMouseDown}
534
+ onMouseUp={handleBackdropMouseUp}
535
+ // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
536
+ tabIndex={0}
537
+ data-test-id={dataTestId}
538
+ style={{
539
+ zIndex: computedZIndex,
540
+ }}
541
+ >
542
+ <CSSTransition
543
+ appear={true}
544
+ timeout={200}
545
+ classNames={styles}
546
+ {...transitionProps}
547
+ in={open}
548
+ onEntered={handleEntered}
549
+ onExited={handleExited}
550
+ >
551
+ <div
552
+ className={cn(styles.component, className)}
553
+ ref={mergeRefs([componentRef, componentNodeRef])}
554
+ >
555
+ <div className={cn(styles.content, contentClassName)}>
556
+ {children}
557
+ </div>
558
+ </div>
559
+ </CSSTransition>
560
+ </div>
561
+ </FocusLock>
562
+ </BaseModalContext.Provider>
563
+ )}
564
+ </Stack>
565
+ );
566
+
567
+ if (!shouldRender) return null;
568
+
569
+ return usePortal ? (
570
+ <Portal getPortalContainer={container} immediateMount={true}>
571
+ {renderContent()}
572
+ </Portal>
573
+ ) : (
574
+ renderContent()
575
+ );
576
+ },
577
+ );
@@ -0,0 +1,60 @@
1
+ @import '@alfalab/core-components-themes/src/default.css';
2
+
3
+ .component {
4
+ position: relative;
5
+ box-sizing: border-box;
6
+ background: var(--color-light-bg-primary);
7
+ margin: auto;
8
+ flex-shrink: 0;
9
+ }
10
+
11
+ .wrapper {
12
+ position: fixed;
13
+ top: 0;
14
+ left: 0;
15
+ right: 0;
16
+ bottom: 0;
17
+
18
+ overflow: auto;
19
+ display: flex;
20
+ flex-direction: column;
21
+ align-items: center;
22
+ outline: 0;
23
+ }
24
+
25
+ .content {
26
+ width: 100%;
27
+ height: 100%;
28
+ display: flex;
29
+ flex-direction: column;
30
+ flex: 1;
31
+ }
32
+
33
+ .hidden {
34
+ display: none;
35
+ }
36
+
37
+ .backdrop {
38
+ z-index: 0;
39
+ }
40
+
41
+ .appear,
42
+ .enter {
43
+ opacity: 0;
44
+ }
45
+
46
+ .appearActive,
47
+ .enterActive {
48
+ opacity: 1;
49
+ transition: opacity 200ms ease-in;
50
+ }
51
+
52
+ .exit {
53
+ opacity: 1;
54
+ }
55
+
56
+ .exitActive,
57
+ .exitDone {
58
+ opacity: 0;
59
+ transition: opacity 200ms ease-out;
60
+ }
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * from './Component';
2
+ export * from './utils';
@@ -0,0 +1,19 @@
1
+ /* eslint-disable */
2
+ // @ts-nocheck
3
+
4
+ if (typeof window !== 'undefined') {
5
+ if (Element && !Element.prototype.matches) {
6
+ Element.prototype.matches =
7
+ Element.prototype.matchesSelector ||
8
+ Element.prototype.mozMatchesSelector ||
9
+ Element.prototype.msMatchesSelector ||
10
+ Element.prototype.oMatchesSelector ||
11
+ Element.prototype.webkitMatchesSelector ||
12
+ function (s) {
13
+ const matches = (this.document || this.ownerDocument).querySelectorAll(s);
14
+ let i = matches.length;
15
+ while (--i >= 0 && matches.item(i) !== this) {}
16
+ return i > -1;
17
+ };
18
+ }
19
+ }
package/src/utils.ts ADDED
@@ -0,0 +1,128 @@
1
+ import { getModalStore, SavedStyle } from '@alfalab/core-components-global-store';
2
+
3
+ export function isScrolledToTop(target: HTMLElement) {
4
+ return target.scrollTop <= 0;
5
+ }
6
+
7
+ export function isScrolledToBottom(target: HTMLElement) {
8
+ return target.scrollHeight - target.offsetHeight <= target.scrollTop;
9
+ }
10
+
11
+ export function hasScrollbar(target: HTMLElement) {
12
+ return target.scrollHeight > target.clientHeight;
13
+ }
14
+
15
+ export const getScrollbarSize = (() => {
16
+ let cachedSize: number;
17
+
18
+ return () => {
19
+ if (cachedSize !== undefined) return cachedSize;
20
+
21
+ const scrollDiv = document.createElement('div');
22
+
23
+ scrollDiv.style.width = '99px';
24
+ scrollDiv.style.height = '99px';
25
+ scrollDiv.style.position = 'absolute';
26
+ scrollDiv.style.top = '-9999px';
27
+ scrollDiv.style.overflow = 'scroll';
28
+
29
+ document.body.appendChild(scrollDiv);
30
+ const scrollbarSize = scrollDiv.offsetWidth - scrollDiv.clientWidth;
31
+
32
+ document.body.removeChild(scrollDiv);
33
+
34
+ cachedSize = scrollbarSize;
35
+
36
+ return scrollbarSize;
37
+ };
38
+ })();
39
+
40
+ const isOverflowing = (container: Element) => {
41
+ if (document.body === container) {
42
+ return window.innerWidth > document.documentElement.clientWidth;
43
+ }
44
+
45
+ return container.scrollHeight > container.clientHeight;
46
+ };
47
+
48
+ const getPaddingRight = (node: Element) =>
49
+ parseInt(window.getComputedStyle(node).paddingRight, 10) || 0;
50
+
51
+ export const restoreContainerStyles = (container: HTMLElement) => {
52
+ const modalRestoreStyles = getModalStore().getRestoreStyles();
53
+
54
+ const index = modalRestoreStyles.findIndex((s) => s.container === container);
55
+ const existingStyles = modalRestoreStyles[index];
56
+
57
+ if (!existingStyles) return;
58
+
59
+ existingStyles.modals -= 1;
60
+
61
+ if (existingStyles.modals <= 0) {
62
+ modalRestoreStyles.splice(index, 1);
63
+
64
+ existingStyles.styles.forEach(({ value, el, key }) => {
65
+ if (value) {
66
+ el.style.setProperty(key, value);
67
+ } else {
68
+ el.style.removeProperty(key);
69
+ }
70
+ });
71
+ }
72
+ };
73
+
74
+ export const handleContainer = (container?: HTMLElement) => {
75
+ if (!container) return;
76
+
77
+ const modalRestoreStyles = getModalStore().getRestoreStyles();
78
+
79
+ const existingStyles = modalRestoreStyles.find((s) => s.container === container);
80
+
81
+ if (existingStyles) {
82
+ existingStyles.modals += 1;
83
+
84
+ return;
85
+ }
86
+
87
+ const containerStyles: SavedStyle[] = [];
88
+
89
+ if (isOverflowing(container)) {
90
+ // Вычисляет размер до применения `overflow hidden` для избежания скачков
91
+ const scrollbarSize = getScrollbarSize();
92
+
93
+ containerStyles.push({
94
+ value: container.style.paddingRight,
95
+ key: 'padding-right',
96
+ el: container,
97
+ });
98
+ // Вычисляем стили, чтобы получить реальный `padding` c шириной сколлбара
99
+ // eslint-disable-next-line no-param-reassign
100
+ container.style.paddingRight = `${getPaddingRight(container) + scrollbarSize}px`;
101
+ }
102
+
103
+ const parent = container.parentElement;
104
+ const scrollContainer =
105
+ // TODO: заменить на optional chaining
106
+ parent &&
107
+ parent.nodeName === 'HTML' &&
108
+ window.getComputedStyle(parent).overflowY === 'scroll'
109
+ ? parent
110
+ : container;
111
+
112
+ // Блокируем скролл даже если отсутствует скроллбар
113
+ if (scrollContainer.style.overflow !== 'hidden') {
114
+ containerStyles.push({
115
+ value: scrollContainer.style.overflow,
116
+ key: 'overflow',
117
+ el: scrollContainer,
118
+ });
119
+ }
120
+
121
+ scrollContainer.style.overflow = 'hidden';
122
+
123
+ modalRestoreStyles.push({
124
+ container,
125
+ modals: 1,
126
+ styles: containerStyles,
127
+ });
128
+ };
package/send-stats.js DELETED
@@ -1,82 +0,0 @@
1
- const http = require('http');
2
- const fs = require('fs');
3
- const { promisify } = require('util');
4
- const path = require('path');
5
-
6
- const readFile = promisify(fs.readFile);
7
-
8
- async function main() {
9
- const remoteHost = process.env.NIS_HOST || 'digital';
10
- const remotePort = process.env.NIS_PORT || 80;
11
- const remotePath = process.env.NIS_PATH || '/npm-install-stats/api/install-stats';
12
-
13
- try {
14
- const [_, node, os, arch] =
15
- /node\/v(\d+\.\d+\.\d+) (\w+) (\w+)/.exec(process.env.npm_config_user_agent) || [];
16
- const [__, npm] = /npm\/(\d+\.\d+\.\d+)/.exec(process.env.npm_config_user_agent) || [];
17
- const [___, yarn] = /yarn\/(\d+\.\d+\.\d+)/.exec(process.env.npm_config_user_agent) || [];
18
-
19
- let ownPackageJson, packageJson;
20
-
21
- try {
22
- const result = await Promise.all([
23
- readFile(path.join(process.cwd(), 'package.json'), 'utf-8'),
24
- readFile(path.join(process.cwd(), '../../../package.json'), 'utf-8'),
25
- ]);
26
-
27
- ownPackageJson = JSON.parse(result[0]);
28
- packageJson = JSON.parse(result[1]);
29
- } catch (err) {
30
- ownPackageJson = '';
31
- packageJson = '';
32
- }
33
-
34
- const data = {
35
- node,
36
- npm,
37
- yarn,
38
- os,
39
- arch,
40
- ownPackageJson: JSON.stringify(ownPackageJson),
41
- packageJson: JSON.stringify(packageJson),
42
- };
43
-
44
- const body = JSON.stringify(data);
45
-
46
- const options = {
47
- host: remoteHost,
48
- port: remotePort,
49
- path: remotePath,
50
- method: 'POST',
51
- headers: {
52
- 'Content-Type': 'application/json',
53
- 'Content-Length': body.length,
54
- },
55
- };
56
-
57
- return new Promise((resolve, reject) => {
58
- const req = http.request(options, (res) => {
59
- res.on('end', () => {
60
- resolve();
61
- });
62
- });
63
-
64
- req.on('error', () => {
65
- reject();
66
- });
67
-
68
- req.write(body);
69
- req.end();
70
- });
71
- } catch (error) {
72
- throw error;
73
- }
74
- }
75
-
76
- main()
77
- .then(() => {
78
- process.exit(0);
79
- })
80
- .catch(() => {
81
- process.exit(0);
82
- });