@alfalab/core-components-notification 6.1.26 → 6.2.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.
@@ -0,0 +1,237 @@
1
+ import React, {
2
+ forwardRef,
3
+ Fragment,
4
+ MouseEvent,
5
+ useCallback,
6
+ useEffect,
7
+ useRef,
8
+ useState,
9
+ } from 'react';
10
+ import mergeRefs from 'react-merge-refs';
11
+ import { useSwipeable } from 'react-swipeable';
12
+ import cn from 'classnames';
13
+ import elementClosest from 'element-closest';
14
+
15
+ import { Portal } from '@alfalab/core-components-portal';
16
+ import { Stack, stackingOrder } from '@alfalab/core-components-stack';
17
+ import {
18
+ ToastPlateDesktop,
19
+ ToastPlateDesktopProps,
20
+ } from '@alfalab/core-components-toast-plate/desktop';
21
+
22
+ import { useClickOutside } from './utils';
23
+
24
+ import styles from './index.module.css';
25
+
26
+ export type NotificationProps = ToastPlateDesktopProps & {
27
+ /**
28
+ * Управление видимостью компонента
29
+ */
30
+ visible?: boolean;
31
+
32
+ /**
33
+ * Отступ от верхнего края
34
+ */
35
+ offset?: number;
36
+
37
+ /**
38
+ * Время до закрытия компонента
39
+ */
40
+ autoCloseDelay?: number | null;
41
+
42
+ /**
43
+ * Использовать портал
44
+ */
45
+ usePortal?: boolean;
46
+
47
+ /**
48
+ * z-index компонента
49
+ */
50
+ zIndex?: number;
51
+
52
+ /**
53
+ * Обработчик события истечения времени до закрытия компонента
54
+ */
55
+ onCloseTimeout?: () => void;
56
+
57
+ /**
58
+ * Обработчик события наведения курсора на компонент
59
+ */
60
+ onMouseEnter?: (event?: MouseEvent<HTMLDivElement>) => void;
61
+
62
+ /**
63
+ * Обработчик события снятия курсора с компонента
64
+ */
65
+ onMouseLeave?: (event?: MouseEvent<HTMLDivElement>) => void;
66
+
67
+ /**
68
+ * Обработчик клика вне компонента
69
+ */
70
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
71
+ onClickOutside?: (event?: MouseEvent<any>) => void;
72
+ };
73
+
74
+ const notificationClassNameSelector = `.${styles.notificationComponent}`;
75
+
76
+ export const Notification = forwardRef<HTMLDivElement, NotificationProps>(
77
+ (
78
+ {
79
+ className,
80
+ actionSectionClassName,
81
+ children,
82
+ visible,
83
+ offset = 108,
84
+ hasCloser = true,
85
+ autoCloseDelay = 5000,
86
+ usePortal = true,
87
+ zIndex = stackingOrder.TOAST,
88
+ style,
89
+ onClose,
90
+ onCloseTimeout,
91
+ onMouseEnter,
92
+ onMouseLeave,
93
+ onClickOutside,
94
+ ...restProps
95
+ },
96
+ ref,
97
+ ) => {
98
+ const notificationRef = useRef<HTMLDivElement>(null);
99
+ const autoCloseTimeoutRef = useRef(0);
100
+ const closeTimeoutRef = useRef(0);
101
+
102
+ const [isClosing, setIsClosing] = useState(false);
103
+
104
+ const startAutoCloseTimer = useCallback(() => {
105
+ if (autoCloseDelay !== null) {
106
+ autoCloseTimeoutRef.current = window.setTimeout(() => {
107
+ if (onCloseTimeout) {
108
+ onCloseTimeout();
109
+ }
110
+ }, autoCloseDelay);
111
+ }
112
+ }, [autoCloseDelay, onCloseTimeout]);
113
+
114
+ const stopAutoCloseTimer = useCallback(() => {
115
+ clearTimeout(autoCloseTimeoutRef.current);
116
+ }, []);
117
+
118
+ useEffect(
119
+ () => () => {
120
+ clearTimeout(closeTimeoutRef.current);
121
+ },
122
+ [],
123
+ );
124
+
125
+ useEffect(() => {
126
+ elementClosest(window);
127
+ }, []);
128
+
129
+ useEffect(() => {
130
+ if (visible) {
131
+ startAutoCloseTimer();
132
+ }
133
+
134
+ return () => {
135
+ stopAutoCloseTimer();
136
+ };
137
+ }, [startAutoCloseTimer, stopAutoCloseTimer, visible]);
138
+
139
+ const handleMouseEnter = useCallback(
140
+ (event: React.MouseEvent<HTMLDivElement>) => {
141
+ stopAutoCloseTimer();
142
+
143
+ if (onMouseEnter) {
144
+ onMouseEnter(event);
145
+ }
146
+ },
147
+ [onMouseEnter, stopAutoCloseTimer],
148
+ );
149
+
150
+ const handleMouseLeave = useCallback(
151
+ (event: React.MouseEvent<HTMLDivElement>) => {
152
+ stopAutoCloseTimer();
153
+ startAutoCloseTimer();
154
+
155
+ if (onMouseLeave) {
156
+ onMouseLeave(event);
157
+ }
158
+ },
159
+ [onMouseLeave, startAutoCloseTimer, stopAutoCloseTimer],
160
+ );
161
+
162
+ const handleOutsideClick = useCallback(
163
+ (event: React.MouseEvent | React.TouchEvent) => {
164
+ const isTargetNotification = !!(event.target as Element).closest(
165
+ notificationClassNameSelector,
166
+ );
167
+
168
+ /*
169
+ * проверка isTargetNotification нужна для предотвращения срабатывания handleOutsideClick
170
+ * при клике на другие нотификации, если их несколько на странице
171
+ */
172
+ if (onClickOutside && visible && !isTargetNotification) {
173
+ onClickOutside(event as React.MouseEvent);
174
+ }
175
+ },
176
+ [onClickOutside, visible],
177
+ );
178
+
179
+ useClickOutside(notificationRef, handleOutsideClick);
180
+
181
+ const swipeableHandlers = useSwipeable({
182
+ onSwiped: ({ dir }) => {
183
+ if (onClose && ['Left', 'Right', 'Up'].includes(dir)) {
184
+ setIsClosing(true);
185
+
186
+ closeTimeoutRef.current = window.setTimeout(() => {
187
+ setIsClosing(false);
188
+ onClose();
189
+ }, 100);
190
+ }
191
+ },
192
+ delta: 100,
193
+ });
194
+
195
+ const Wrapper = usePortal ? Portal : Fragment;
196
+
197
+ return (
198
+ <Stack value={zIndex}>
199
+ {(computedZIndex) => (
200
+ <Wrapper>
201
+ <div {...swipeableHandlers}>
202
+ <ToastPlateDesktop
203
+ className={cn(
204
+ styles.notificationComponent,
205
+ {
206
+ [styles.isVisible]: visible,
207
+ [styles.isClosing]: isClosing,
208
+ },
209
+ className,
210
+ )}
211
+ contentClassName={styles.toastContent}
212
+ actionSectionClassName={cn(
213
+ actionSectionClassName,
214
+ styles.actionSection,
215
+ )}
216
+ style={{
217
+ top: offset,
218
+ zIndex: computedZIndex,
219
+ ...style,
220
+ }}
221
+ onMouseEnter={handleMouseEnter}
222
+ onMouseLeave={handleMouseLeave}
223
+ ref={mergeRefs([ref, notificationRef])}
224
+ role={visible ? 'alert' : undefined}
225
+ hasCloser={hasCloser}
226
+ onClose={onClose}
227
+ {...restProps}
228
+ >
229
+ {children}
230
+ </ToastPlateDesktop>
231
+ </div>
232
+ </Wrapper>
233
+ )}
234
+ </Stack>
235
+ );
236
+ },
237
+ );
@@ -0,0 +1,50 @@
1
+ @import '@alfalab/core-components-themes/src/default.css';
2
+
3
+ :root {
4
+ --notification-desktop-content-width: 278px;
5
+ }
6
+
7
+ .notificationComponent {
8
+ visibility: hidden;
9
+ position: fixed;
10
+ right: var(--gap-s);
11
+ transform: translate(0, -500px);
12
+ display: inline-flex;
13
+ width: calc(100% - var(--gap-xl));
14
+ max-width: calc(100vw - var(--gap-xl));
15
+ user-select: none;
16
+ transition: transform 0.4s ease-out;
17
+
18
+ @media screen and (min-width: 600px) {
19
+ right: var(--gap-4xl);
20
+ width: auto;
21
+ transform: translate(calc(100% + var(--gap-4xl)), 0);
22
+ }
23
+
24
+ &.isVisible {
25
+ visibility: visible;
26
+ transform: translate(0, 0);
27
+ }
28
+
29
+ &.isClosing {
30
+ transition: transform 0.1s ease-out;
31
+ transform: translate(100vw, 0);
32
+
33
+ @media screen and (min-width: 600px) {
34
+ transform: translate(calc(100% + var(--gap-4xl)), 0);
35
+ }
36
+ }
37
+ }
38
+
39
+ .toastContent {
40
+ @media screen and (min-width: 600px) {
41
+ width: var(--notification-desktop-content-width);
42
+ }
43
+ }
44
+
45
+ .actionSection {
46
+ min-width: 104px;
47
+ min-height: 48px;
48
+ padding: 0 var(--gap-xs);
49
+ margin: var(--gap-s-neg) 0;
50
+ }
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export * from './Component';
@@ -0,0 +1,31 @@
1
+ import React from 'react';
2
+
3
+ /*
4
+ * Дублирую хук из @alfalab/hooks, так как не подходят названия событий
5
+ * https://github.com/core-ds/utils/blob/develop/packages/hooks/src/useClickOutside/hook.ts
6
+ * issue завел, когда изменения будут реализованы, отсюда можно будет удалить
7
+ * https://github.com/core-ds/utils/issues/35
8
+ */
9
+ export function useClickOutside(
10
+ ref: React.RefObject<HTMLElement>,
11
+ cb: (e: React.MouseEvent | React.TouchEvent) => void,
12
+ ): void {
13
+ React.useEffect(() => {
14
+ // eslint-disable-next-line
15
+ const handler = (event: any) => {
16
+ if (!ref.current || ref.current.contains(event.target)) {
17
+ return;
18
+ }
19
+
20
+ cb(event);
21
+ };
22
+
23
+ document.addEventListener('click', handler);
24
+ document.addEventListener('touchend', handler);
25
+
26
+ return () => {
27
+ document.removeEventListener('click', handler);
28
+ document.removeEventListener('touchend', handler);
29
+ };
30
+ }, [ref, cb]);
31
+ }
@@ -0,0 +1,95 @@
1
+ import { AnchorHTMLAttributes, ButtonHTMLAttributes, ElementType, ReactNode } from 'react';
2
+ type StyleColors = {
3
+ default: {
4
+ [key: string]: string;
5
+ };
6
+ inverted: {
7
+ [key: string]: string;
8
+ };
9
+ };
10
+ type ComponentProps = {
11
+ /**
12
+ * Тип кнопки
13
+ * @default secondary
14
+ */
15
+ view?: 'accent' | 'primary' | 'secondary' | 'tertiary' | 'outlined' | 'filled' | 'transparent' | 'link' | 'ghost';
16
+ /**
17
+ * Слот слева
18
+ */
19
+ leftAddons?: ReactNode;
20
+ /**
21
+ * Слот справа
22
+ */
23
+ rightAddons?: ReactNode;
24
+ /**
25
+ * Размер компонента
26
+ * @default m
27
+ */
28
+ size?: 'xxs' | 'xs' | 's' | 'm' | 'l' | 'xl';
29
+ /**
30
+ * Растягивает компонент на ширину контейнера
31
+ * @default false
32
+ */
33
+ block?: boolean;
34
+ /**
35
+ * Дополнительный класс
36
+ */
37
+ className?: string;
38
+ /**
39
+ * Дополнительный класс для спиннера
40
+ */
41
+ spinnerClassName?: string;
42
+ /**
43
+ * Выводит ссылку в виде кнопки
44
+ */
45
+ href?: string;
46
+ /**
47
+ * Позволяет использовать кастомный компонент для кнопки (например Link из роутера)
48
+ */
49
+ Component?: ElementType;
50
+ /**
51
+ * Идентификатор для систем автоматизированного тестирования
52
+ */
53
+ dataTestId?: string;
54
+ /**
55
+ * Показать лоадер
56
+ * @default false
57
+ */
58
+ loading?: boolean;
59
+ /**
60
+ * Не переносить текст кнопки на новую строку
61
+ * @default false
62
+ */
63
+ nowrap?: boolean;
64
+ /**
65
+ * Набор цветов для компонента
66
+ */
67
+ colors?: 'default' | 'inverted';
68
+ /**
69
+ * Дочерние элементы.
70
+ */
71
+ children?: ReactNode;
72
+ /**
73
+ * Основные стили компонента.
74
+ */
75
+ styles: {
76
+ [key: string]: string;
77
+ };
78
+ /**
79
+ * Стили компонента для default и inverted режима.
80
+ */
81
+ colorStylesMap: StyleColors;
82
+ };
83
+ type AnchorBaseButtonProps = ComponentProps & AnchorHTMLAttributes<HTMLAnchorElement>;
84
+ type NativeBaseButtonProps = ComponentProps & ButtonHTMLAttributes<HTMLButtonElement>;
85
+ type BaseButtonProps = Partial<AnchorBaseButtonProps | NativeBaseButtonProps>;
86
+ type AnchorButtonProps = Omit<BaseButtonProps, 'styles' | 'colorStylesMap'> & AnchorHTMLAttributes<HTMLAnchorElement>;
87
+ type NativeButtonProps = Omit<BaseButtonProps, 'styles' | 'colorStylesMap'> & ButtonHTMLAttributes<HTMLButtonElement>;
88
+ type ButtonProps = Partial<AnchorButtonProps | NativeButtonProps> & {
89
+ /**
90
+ * Контрольная точка, с нее начинается desktop версия
91
+ * @default 1024
92
+ */
93
+ breakpoint?: number;
94
+ };
95
+ export { StyleColors, ComponentProps, AnchorBaseButtonProps, NativeBaseButtonProps, BaseButtonProps, AnchorButtonProps, NativeButtonProps, ButtonProps };
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
- });