@astral/ui 4.41.0 → 4.42.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.
Files changed (35) hide show
  1. package/components/Notification/Notification.js +1 -0
  2. package/components/Notification/NotificationContainer/styles.js +16 -5
  3. package/components/Notification/NotificationStackContainer/NotificationStackContainer.js +3 -3
  4. package/components/Notification/NotificationStackContainer/styles.js +261 -23
  5. package/components/Notification/NotificationStackContainer/useLogic/hooks/index.d.ts +1 -0
  6. package/components/Notification/NotificationStackContainer/useLogic/hooks/index.js +1 -0
  7. package/components/Notification/NotificationStackContainer/useLogic/hooks/useHover/useHover.d.ts +5 -1
  8. package/components/Notification/NotificationStackContainer/useLogic/hooks/useHover/useHover.js +6 -2
  9. package/components/Notification/NotificationStackContainer/useLogic/hooks/useTouchStackExpand/index.d.ts +1 -0
  10. package/components/Notification/NotificationStackContainer/useLogic/hooks/useTouchStackExpand/index.js +1 -0
  11. package/components/Notification/NotificationStackContainer/useLogic/hooks/useTouchStackExpand/useTouchStackExpand.d.ts +10 -0
  12. package/components/Notification/NotificationStackContainer/useLogic/hooks/useTouchStackExpand/useTouchStackExpand.js +54 -0
  13. package/components/Notification/NotificationStackContainer/useLogic/useLogic.d.ts +1 -1
  14. package/components/Notification/NotificationStackContainer/useLogic/useLogic.js +19 -9
  15. package/components/Notification/NotificationTemplate/styles.js +20 -0
  16. package/components/Notification/constants.d.ts +4 -0
  17. package/components/Notification/constants.js +4 -0
  18. package/node/components/Notification/Notification.js +1 -0
  19. package/node/components/Notification/NotificationContainer/styles.js +15 -4
  20. package/node/components/Notification/NotificationStackContainer/NotificationStackContainer.js +3 -3
  21. package/node/components/Notification/NotificationStackContainer/styles.js +260 -22
  22. package/node/components/Notification/NotificationStackContainer/useLogic/hooks/index.d.ts +1 -0
  23. package/node/components/Notification/NotificationStackContainer/useLogic/hooks/index.js +1 -0
  24. package/node/components/Notification/NotificationStackContainer/useLogic/hooks/useHover/useHover.d.ts +5 -1
  25. package/node/components/Notification/NotificationStackContainer/useLogic/hooks/useHover/useHover.js +6 -2
  26. package/node/components/Notification/NotificationStackContainer/useLogic/hooks/useTouchStackExpand/index.d.ts +1 -0
  27. package/node/components/Notification/NotificationStackContainer/useLogic/hooks/useTouchStackExpand/index.js +17 -0
  28. package/node/components/Notification/NotificationStackContainer/useLogic/hooks/useTouchStackExpand/useTouchStackExpand.d.ts +10 -0
  29. package/node/components/Notification/NotificationStackContainer/useLogic/hooks/useTouchStackExpand/useTouchStackExpand.js +58 -0
  30. package/node/components/Notification/NotificationStackContainer/useLogic/useLogic.d.ts +1 -1
  31. package/node/components/Notification/NotificationStackContainer/useLogic/useLogic.js +18 -8
  32. package/node/components/Notification/NotificationTemplate/styles.js +20 -0
  33. package/node/components/Notification/constants.d.ts +4 -0
  34. package/node/components/Notification/constants.js +5 -1
  35. package/package.json +1 -1
@@ -5,6 +5,7 @@ import { getClassNameModifierByVariant, getNotifyOptions } from './utils';
5
5
  const leave = cssTransition({
6
6
  enter: notifyClassnames.animationIn,
7
7
  exit: notifyClassnames.animationOut,
8
+ collapse: false,
8
9
  });
9
10
  export const notify = {
10
11
  success: (title, { icon, ...options } = {}) => toast(({ toastProps }) => NOTIFICATION_VARIANT.success({ ...options, icon, title }, toastProps), {
@@ -1,6 +1,6 @@
1
1
  import { ToastContainer } from 'react-toastify-next';
2
2
  import { keyframes, styled } from '../../styled';
3
- import { NOTIFY_HEIGHT, NotificationVariantTypes, notifyClassnames, } from '../constants';
3
+ import { NOTIFY_HEIGHT, NOTIFY_HEIGHT_MOBILE, NotificationVariantTypes, notifyClassnames, } from '../constants';
4
4
  const leaveIn = keyframes `
5
5
  from {
6
6
  transform: translateX(100%);
@@ -46,23 +46,28 @@ export const Wrapper = styled('div', {
46
46
  `}
47
47
  }
48
48
 
49
+ .Toastify__toast-container {
50
+ ${({ theme }) => theme.breakpoints.down('sm')} {
51
+ padding: ${({ theme }) => theme.spacing(4)};
52
+ }
53
+ }
54
+
49
55
  .${notifyClassnames.animationIn} {
50
- animation: ${leaveIn} ease-in-out 0.34s;
56
+ animation: ${leaveIn} ease-in-out ${({ theme }) => theme.transitions.duration.standard}ms;
51
57
  animation-fill-mode: both;
52
58
 
53
59
  ${({ theme }) => theme.breakpoints.down('sm')} {
54
- animation: ${leaveInMobile} ease-in-out 0.34s;
60
+ animation: ${leaveInMobile} ease-in-out ${({ theme }) => theme.transitions.duration.standard}ms;
55
61
  animation-fill-mode: both;
56
62
  }
57
63
  }
58
64
 
59
65
  .${notifyClassnames.animationOut} {
60
- animation: ${leaveOut} ease-in-out 0.34s;
66
+ animation: ${leaveOut} ease-in-out ${({ theme }) => theme.transitions.duration.standard}ms;
61
67
  animation-fill-mode: both;
62
68
  }
63
69
 
64
70
  .${notifyClassnames.root} {
65
- min-height: ${NOTIFY_HEIGHT};
66
71
  margin-bottom: ${({ theme }) => theme.spacing(1)};
67
72
  padding: 0;
68
73
 
@@ -81,6 +86,12 @@ export const Wrapper = styled('div', {
81
86
  width: 100%;
82
87
  height: 3px;
83
88
  }
89
+ ${({ theme }) => theme.breakpoints.up('sm')} {
90
+ min-height: ${NOTIFY_HEIGHT};
91
+ }
92
+ ${({ theme }) => theme.breakpoints.down('sm')} {
93
+ min-height: ${NOTIFY_HEIGHT_MOBILE};
94
+ }
84
95
 
85
96
  ${({ theme }) => theme.breakpoints.down('sm')} {
86
97
  margin-bottom: ${({ theme }) => theme.spacing(3)};
@@ -8,12 +8,12 @@ if (typeof window !== 'undefined') {
8
8
  injectStyle();
9
9
  }
10
10
  export const NotificationStackContainer = ({ containerId = NOTIFY_CONTAINER_ID, staticContainerId = NOTIFY_STATIC_CONTAINER_ID, bannerContainer, closeButton, ...props }) => {
11
- const { isVisibleCloseButton, isHoveredContainer, isStartedClosingNotify, closeAll, } = useLogic({
11
+ const { isVisibleCloseButton, isStackExpanded, isStartedClosingNotify, closeAll, } = useLogic({
12
12
  containerId,
13
13
  });
14
14
  return (_jsxs(Wrapper, { className: classNames({
15
- [notifyClassnames.container]: isHoveredContainer,
16
- }), children: [_jsx(Inner, { ...props, ...{ stacked: true }, containerId: containerId, pauseOnFocusLoss: true, position: NOTIFY_POSITIONS.BOTTOM_RIGHT, newestOnTop: false, closeOnClick: false, draggable: false, rtl: false, closeButton: false, bodyClassName: `${notifyClassnames.body}`, toastClassName: notifyClassnames.root, progressClassName: `${notifyClassnames.progress}` }), isVisibleCloseButton && (_jsx(CloseButton, { size: "small", variant: "light", color: "grey", className: classNames({
15
+ [notifyClassnames.container]: isStackExpanded,
16
+ }), children: [_jsx("div", { className: notifyClassnames.stackHost, children: _jsx(Inner, { ...props, className: classNames(notifyClassnames.stack, props.className), ...{ stacked: true }, containerId: containerId, pauseOnFocusLoss: true, position: NOTIFY_POSITIONS.BOTTOM_RIGHT, newestOnTop: false, closeOnClick: false, draggable: false, rtl: false, closeButton: false, bodyClassName: `${notifyClassnames.body}`, toastClassName: notifyClassnames.root, progressClassName: `${notifyClassnames.progress}` }) }), isVisibleCloseButton && (_jsx(CloseButton, { size: "small", variant: "light", color: "grey", className: classNames({
17
17
  [notifyClassnames.closeButtonAnimationOut]: isStartedClosingNotify,
18
18
  }), onClick: closeAll, children: "\u0421\u043A\u0440\u044B\u0442\u044C \u0432\u0441\u0435" })), _jsx(StaticInner, { containerId: staticContainerId, position: NOTIFY_POSITIONS.BOTTOM_RIGHT, newestOnTop: false, closeOnClick: false, draggable: false, rtl: false, closeButton: false, bodyClassName: `${notifyClassnames.body}`, toastClassName: notifyClassnames.root, progressClassName: `${notifyClassnames.progress}` }), bannerContainer] }));
19
19
  };
@@ -1,7 +1,7 @@
1
1
  import { ToastContainer } from 'react-toastify-next';
2
2
  import { Button } from '../../Button';
3
3
  import { keyframes, styled } from '../../styled';
4
- import { NOTIFY_HEIGHT, NOTIFY_NO_TRANSITION_ATTR, NotificationVariantTypes, notifyClassnames, } from '../constants';
4
+ import { NOTIFY_HEIGHT, NOTIFY_HEIGHT_MOBILE, NOTIFY_NO_TRANSITION_ATTR, NotificationVariantTypes, notifyClassnames, } from '../constants';
5
5
  const fade = keyframes `
6
6
  from {
7
7
  opacity: 0;
@@ -30,6 +30,16 @@ const leaveOut = keyframes `
30
30
  visibility: hidden;
31
31
  }
32
32
  `;
33
+ /** Сохраняем scale stacked (--s), иначе при старте exit transform из keyframes затирает scale и карточка визуально «прыгает» вправо */
34
+ const leaveOutMobileStacked = keyframes `
35
+ from {
36
+ transform: translateX(0) scale(var(--s, 1));
37
+ }
38
+ to {
39
+ transform: translateX(100%) scale(var(--s, 1));
40
+ visibility: hidden;
41
+ }
42
+ `;
33
43
  // используем дополнительный враппер,
34
44
  // потому что styled для ToastContainer не умеет работать с theme внутри
35
45
  // использовать бэм классы для стилизации пришлось,
@@ -45,7 +55,15 @@ export const Wrapper = styled.div `
45
55
  width: 432px;
46
56
 
47
57
  ${({ theme }) => theme.breakpoints.down('sm')} {
48
- right: -20px;
58
+ inset: 0 0 auto;
59
+
60
+ width: 100%;
61
+ max-width: 100vw;
62
+ padding: ${({ theme }) => theme.spacing(2, 2, 2, 2)};
63
+ padding-top: max(
64
+ ${({ theme }) => theme.spacing(2)},
65
+ env(safe-area-inset-top, 0px)
66
+ );
49
67
  }
50
68
 
51
69
  &.${notifyClassnames.container} {
@@ -62,18 +80,86 @@ export const Wrapper = styled.div `
62
80
  }
63
81
  /* stylelint-enable plugin/no-unsupported-browser-features */
64
82
 
65
- .Toastify:not(:last-child) {
66
- overflow: hidden;
83
+ ${({ theme }) => theme.breakpoints.up('sm')} {
84
+ .${notifyClassnames.stackHost} .Toastify {
85
+ overflow: hidden;
86
+ }
87
+ }
88
+
89
+ ${({ theme }) => theme.breakpoints.down('sm')} {
90
+ justify-content: flex-start;
91
+
92
+ height: auto;
93
+
94
+ .${notifyClassnames.stackHost} .Toastify {
95
+ overflow: visible;
96
+ }
97
+
98
+ .${notifyClassnames.stack} {
99
+ overflow: hidden auto;
100
+ display: flex;
101
+ flex-direction: column;
102
+ gap: ${({ theme }) => theme.spacing(2)};
103
+ align-items: stretch;
104
+
105
+ width: 100%;
106
+ max-width: 100%;
107
+ padding-top: ${({ theme }) => theme.microSpacing(1)};
108
+ padding-bottom: 8px !important;
109
+ padding-bottom: ${({ theme }) => theme.spacing(1)};
110
+ -webkit-overflow-scrolling: touch;
111
+ }
112
+
113
+ .${notifyClassnames.stack} .${notifyClassnames.root} {
114
+ pointer-events: auto !important;
115
+
116
+ position: relative !important;
117
+ transform: none !important;
118
+
119
+ flex-shrink: 0;
120
+
121
+ width: 100% !important;
122
+ max-width: 100%;
123
+ height: auto !important;
124
+ min-height: ${NOTIFY_HEIGHT_MOBILE};
125
+ max-height: fit-content !important;
126
+ margin-top: 0 !important;
127
+ margin-right: 0 !important;
128
+ margin-bottom: 0 !important;
129
+
130
+ opacity: 1 !important;
131
+ }
132
+
133
+ /*
134
+ На мобильных контейнер не получает mouseEnter — toastify оставляет data-collapsed=true
135
+ и скрывает direct children не‑последних тостов (.Toastify__toast--stacked[data-collapsed=true]:not(:last-child)>*{opacity:0})
136
+ */
137
+ .${notifyClassnames.stack} .Toastify__toast--stacked > * {
138
+ opacity: 1 !important;
139
+ }
140
+
141
+ .${notifyClassnames.stack} .${notifyClassnames.root}__content {
142
+ max-height: 400px;
143
+ margin: ${({ theme }) => theme.spacing(2, 0, 0, 8)};
144
+ }
145
+
146
+ .${notifyClassnames.stack} .${notifyClassnames.root}__footer {
147
+ margin-bottom: 0;
148
+ }
149
+
150
+ .${notifyClassnames.stack} .Toastify__toast--stacked::before {
151
+ display: none;
152
+ }
67
153
  }
68
154
  }
69
155
 
70
156
  .${notifyClassnames.animationIn} {
71
- animation: ${leaveIn} ease-in-out 0.34s;
157
+ animation: ${leaveIn} ease-in-out ${({ theme }) => theme.transitions.duration.standard}ms;
72
158
  animation-fill-mode: both;
73
159
  }
74
160
 
75
161
  .${notifyClassnames.animationOut} {
76
- animation: ${leaveOut} ease-in-out 0.34s;
162
+ animation: ${leaveOut} ease-in-out;
77
163
  animation-fill-mode: both;
78
164
  }
79
165
 
@@ -82,7 +168,6 @@ export const Wrapper = styled.div `
82
168
 
83
169
  width: 400px;
84
170
  height: ${NOTIFY_HEIGHT};
85
- min-height: ${NOTIFY_HEIGHT};
86
171
  padding: 0;
87
172
 
88
173
  box-shadow: ${({ theme }) => theme.elevation[300]};
@@ -104,6 +189,12 @@ export const Wrapper = styled.div `
104
189
  width: 100%;
105
190
  height: 3px;
106
191
  }
192
+ ${({ theme }) => theme.breakpoints.up('sm')} {
193
+ min-height: ${NOTIFY_HEIGHT};
194
+ }
195
+ ${({ theme }) => theme.breakpoints.down('sm')} {
196
+ min-height: ${NOTIFY_HEIGHT_MOBILE};
197
+ }
107
198
 
108
199
  .Toastify__progress-bar--wrp {
109
200
  top: 0;
@@ -151,8 +242,8 @@ export const Wrapper = styled.div `
151
242
  }
152
243
 
153
244
  /* Стили для уведомлений в стеке */
154
- .Toastify:not(:last-child) {
155
- .${notifyClassnames.root} {
245
+ ${({ theme }) => theme.breakpoints.up('sm')} {
246
+ .${notifyClassnames.stack} .${notifyClassnames.root} {
156
247
  position: relative;
157
248
  transform: scale(var(--s));
158
249
 
@@ -185,6 +276,119 @@ export const Wrapper = styled.div `
185
276
  }
186
277
  }
187
278
  }
279
+
280
+ /* Мобильный стек в свёрнутом виде: последний в DOM (самый новый) — сверху; остальные без участия в раскладке */
281
+ ${({ theme }) => theme.breakpoints.down('sm')} {
282
+ &:not(.${notifyClassnames.container}) {
283
+ .${notifyClassnames.stack} {
284
+ position: relative;
285
+
286
+ overflow: visible;
287
+ display: flex;
288
+ flex-direction: column-reverse;
289
+ gap: 0;
290
+ align-items: stretch;
291
+ justify-content: flex-end;
292
+
293
+ max-height: none;
294
+ margin-bottom: ${({ theme }) => theme.spacing(2)};
295
+ -webkit-overflow-scrolling: auto;
296
+
297
+ /* Два фейковых слоя — только пока в стеке ≥2 активных тоста (без анимации закрытия) */
298
+
299
+ /* Причина игнора: Не критично для отображения */
300
+ /* stylelint-disable-next-line plugin/no-unsupported-browser-features */
301
+ &:has(.${notifyClassnames.root}:not(.${notifyClassnames.animationOut}) ~ .${notifyClassnames.root}:not(.${notifyClassnames.animationOut})) {
302
+ padding-top: ${({ theme }) => theme.spacing(4)};
303
+
304
+ &::before,
305
+ &::after {
306
+ pointer-events: none;
307
+ content: '';
308
+
309
+ position: absolute;
310
+ z-index: 0;
311
+ right: ${({ theme }) => theme.spacing(2)};
312
+ left: ${({ theme }) => theme.spacing(2)};
313
+
314
+ height: ${NOTIFY_HEIGHT_MOBILE};
315
+
316
+ background: ${({ theme }) => theme.palette.background.paper};
317
+ border-radius: ${({ theme }) => theme.shape.medium};
318
+ box-shadow: ${({ theme }) => theme.elevation[200]};
319
+ }
320
+
321
+ &::before {
322
+ top: 0;
323
+ transform: scaleX(0.92);
324
+ }
325
+
326
+ &::after {
327
+ top: ${({ theme }) => theme.spacing(2)};
328
+ transform: scaleX(0.96);
329
+ }
330
+ }
331
+ }
332
+
333
+ .${notifyClassnames.stack} .${notifyClassnames.root}:not(:last-of-type) {
334
+ pointer-events: none;
335
+
336
+ position: absolute;
337
+
338
+ overflow: hidden;
339
+
340
+ width: 1px;
341
+ height: 1px;
342
+ margin: -1px;
343
+ padding: 0;
344
+
345
+ white-space: nowrap;
346
+
347
+ opacity: 0;
348
+ clip: rect(0, 0, 0, 0);
349
+ clip-path: inset(50%);
350
+ border: 0;
351
+
352
+ .${notifyClassnames.root}__content {
353
+ max-height: 0 !important;
354
+ margin: 0 !important;
355
+ }
356
+ }
357
+
358
+ .${notifyClassnames.stack} .${notifyClassnames.root}:last-of-type {
359
+ position: relative;
360
+ z-index: 1;
361
+ transform: scale(var(--s, 1));
362
+
363
+ flex: 0 0 auto;
364
+ align-self: stretch;
365
+
366
+ width: 100% !important;
367
+ min-width: 0 !important;
368
+ max-width: 100% !important;
369
+ min-height: ${NOTIFY_HEIGHT_MOBILE};
370
+ margin-bottom: 0 !important;
371
+
372
+ opacity: 1;
373
+ border-radius: ${({ theme }) => theme.shape.medium};
374
+
375
+ .${notifyClassnames.root}__content {
376
+ max-height: 400px;
377
+ margin: ${({ theme }) => theme.spacing(2, 0, 0, 8)};
378
+ }
379
+ }
380
+
381
+ .${notifyClassnames.stackHost} .Toastify,
382
+ .${notifyClassnames.stackHost} .${notifyClassnames.stack} {
383
+ overflow: visible !important;
384
+ }
385
+
386
+ .${notifyClassnames.animationOut} {
387
+ animation: ${leaveOutMobileStacked} ease-in-out ${({ theme }) => theme.transitions.duration.standard}ms;
388
+ animation-fill-mode: both;
389
+ }
390
+ }
391
+ }
188
392
  `;
189
393
  export const Inner = styled(ToastContainer) `
190
394
  position: relative !important;
@@ -205,20 +409,53 @@ export const Inner = styled(ToastContainer) `
205
409
  }
206
410
  }
207
411
 
208
- &:hover {
209
- overflow: hidden auto;
412
+ /* Только устройства с реальным hover и ≥sm — иначе на touch :hover залипает и ломает мобильный стек */
413
+ ${({ theme }) => theme.breakpoints.up('sm')} {
414
+ @media (hover: hover) and (pointer: fine) {
415
+ &:hover {
416
+ overflow: hidden auto;
210
417
 
211
- height: 100%;
418
+ height: 100%;
212
419
 
213
- .${notifyClassnames.root} {
214
- position: relative;
215
- transform: unset;
420
+ .${notifyClassnames.root} {
421
+ position: relative;
422
+ transform: unset;
216
423
 
217
- height: auto;
218
- min-height: ${NOTIFY_HEIGHT};
219
- margin-bottom: ${({ theme }) => theme.spacing(1)} !important;
424
+ height: auto;
425
+ min-height: ${NOTIFY_HEIGHT};
426
+ margin-bottom: ${({ theme }) => theme.spacing(1)} !important;
220
427
 
221
- opacity: 1 !important;
428
+ opacity: 1 !important;
429
+ }
430
+
431
+ .${notifyClassnames.root}__content {
432
+ max-height: 400px;
433
+ margin: ${({ theme }) => theme.spacing(2, 0, 0, 8)};
434
+ }
435
+
436
+ .${notifyClassnames.root}__footer {
437
+ margin-bottom: 0;
438
+ }
439
+
440
+ .Toastify__toast--stacked::before {
441
+ display: none;
442
+ }
443
+ }
444
+ }
445
+ }
446
+
447
+ ${({ theme }) => theme.breakpoints.down('sm')} {
448
+ min-width: unset;
449
+ max-width: 100%;
450
+ padding-right: ${({ theme }) => theme.spacing(2)} !important;
451
+ padding-left: ${({ theme }) => theme.spacing(2)} !important;
452
+
453
+ .${notifyClassnames.root} {
454
+ width: 100% !important;
455
+ max-width: 100%;
456
+ height: auto !important;
457
+ min-height: ${NOTIFY_HEIGHT_MOBILE};
458
+ margin-bottom: ${({ theme }) => theme.spacing(2)} !important;
222
459
  }
223
460
 
224
461
  .${notifyClassnames.root}__content {
@@ -229,10 +466,6 @@ export const Inner = styled(ToastContainer) `
229
466
  .${notifyClassnames.root}__footer {
230
467
  margin-bottom: 0;
231
468
  }
232
-
233
- .Toastify__toast--stacked::before {
234
- display: none;
235
- }
236
469
  }
237
470
  `;
238
471
  export const StaticInner = styled(Inner) `
@@ -259,4 +492,9 @@ export const CloseButton = styled(Button) `
259
492
  &.${notifyClassnames.closeButtonAnimationOut} {
260
493
  opacity: 0;
261
494
  }
495
+
496
+ ${({ theme }) => theme.breakpoints.down('sm')} {
497
+ width: calc(100% - ${({ theme }) => theme.spacing(4)});
498
+ margin: ${({ theme }) => theme.spacing(0, 5, 2, 5)};
499
+ }
262
500
  `;
@@ -1 +1,2 @@
1
1
  export * from './useHover';
2
+ export * from './useTouchStackExpand';
@@ -1 +1,2 @@
1
1
  export * from './useHover';
2
+ export * from './useTouchStackExpand';
@@ -1,3 +1,7 @@
1
- export declare const useHover: (element?: Element) => {
1
+ type UseHoverParams = {
2
+ enabled?: boolean;
3
+ };
4
+ export declare const useHover: (element: Element | undefined, { enabled }?: UseHoverParams) => {
2
5
  isHovered: boolean;
3
6
  };
7
+ export {};
@@ -1,9 +1,13 @@
1
1
  import { useEffect, useState } from 'react';
2
- export const useHover = (element) => {
2
+ export const useHover = (element, { enabled = true } = {}) => {
3
3
  const [isHovered, setHover] = useState(false);
4
4
  const handleMouseEnter = () => setHover(true);
5
5
  const handleMouseLeave = () => setHover(false);
6
6
  useEffect(() => {
7
+ if (!enabled) {
8
+ setHover(false);
9
+ return undefined;
10
+ }
7
11
  if (!element) {
8
12
  return undefined;
9
13
  }
@@ -13,6 +17,6 @@ export const useHover = (element) => {
13
17
  element.removeEventListener('mouseenter', handleMouseEnter);
14
18
  element.removeEventListener('mouseleave', handleMouseLeave);
15
19
  };
16
- }, [element]);
20
+ }, [element, enabled]);
17
21
  return { isHovered };
18
22
  };
@@ -0,0 +1,10 @@
1
+ type UseTouchStackExpandParams = {
2
+ stackContainerElement: Element | null;
3
+ enabled: boolean;
4
+ hasOpenNotify: boolean;
5
+ };
6
+ /**
7
+ * Определяет, является ли стек уведомлений раскрытым на мобильном устройстве.
8
+ */
9
+ export declare const useTouchStackExpand: ({ stackContainerElement, enabled, hasOpenNotify, }: UseTouchStackExpandParams) => boolean;
10
+ export {};
@@ -0,0 +1,54 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { notifyClassnames } from '../../../../constants';
3
+ const isInteractiveTarget = (element) => Boolean(element.closest('button, a[href], [role="button"], input, textarea, select'));
4
+ /**
5
+ * Определяет, является ли стек уведомлений раскрытым на мобильном устройстве.
6
+ */
7
+ export const useTouchStackExpand = ({ stackContainerElement, enabled, hasOpenNotify, }) => {
8
+ const [isTouchExpanded, setTouchExpanded] = useState(false);
9
+ useEffect(() => {
10
+ if (!enabled || !hasOpenNotify) {
11
+ setTouchExpanded(false);
12
+ }
13
+ }, [enabled, hasOpenNotify]);
14
+ useEffect(() => {
15
+ if (!enabled || !hasOpenNotify || !stackContainerElement) {
16
+ return undefined;
17
+ }
18
+ const toggleStack = (event) => {
19
+ const { target } = event;
20
+ if (!(target instanceof Element) || isInteractiveTarget(target)) {
21
+ return;
22
+ }
23
+ setTouchExpanded((prev) => !prev);
24
+ };
25
+ stackContainerElement.addEventListener('pointerdown', toggleStack);
26
+ return () => {
27
+ stackContainerElement.removeEventListener('pointerdown', toggleStack);
28
+ };
29
+ }, [enabled, hasOpenNotify, stackContainerElement]);
30
+ useEffect(() => {
31
+ const collapseIfOutside = (event) => {
32
+ const { target } = event;
33
+ if (!(target instanceof Node) || !stackContainerElement) {
34
+ return;
35
+ }
36
+ const stackHost = stackContainerElement.closest(`.${notifyClassnames.stackHost}`);
37
+ const insideUi = stackHost?.parentElement ?? stackContainerElement;
38
+ if (!insideUi.contains(target)) {
39
+ setTouchExpanded(false);
40
+ }
41
+ };
42
+ document.addEventListener('pointerdown', collapseIfOutside, true);
43
+ if (!enabled ||
44
+ !hasOpenNotify ||
45
+ !isTouchExpanded ||
46
+ !stackContainerElement) {
47
+ document.removeEventListener('pointerdown', collapseIfOutside, true);
48
+ }
49
+ return () => {
50
+ document.removeEventListener('pointerdown', collapseIfOutside, true);
51
+ };
52
+ }, [enabled, hasOpenNotify, isTouchExpanded, stackContainerElement]);
53
+ return isTouchExpanded;
54
+ };
@@ -4,7 +4,7 @@ type UseLogicParams = {
4
4
  };
5
5
  export declare const useLogic: ({ containerId: externalContainerId, }: UseLogicParams) => {
6
6
  isVisibleCloseButton: boolean;
7
- isHoveredContainer: boolean;
7
+ isStackExpanded: boolean;
8
8
  isStartedClosingNotify: boolean;
9
9
  closeAll: () => void;
10
10
  };
@@ -1,9 +1,11 @@
1
1
  import { useEffect, useState } from 'react';
2
2
  import { toast } from 'react-toastify-next';
3
+ import { useViewportType } from '../../../useViewportType';
3
4
  import { NOTIFY_NO_TRANSITION_ATTR, notifyClassnames } from '../../constants';
4
5
  import { sleep } from '../../utils/sleep';
5
- import { useHover } from './hooks';
6
+ import { useHover, useTouchStackExpand } from './hooks';
6
7
  export const useLogic = ({ containerId: externalContainerId, }) => {
8
+ const { isMobile } = useViewportType();
7
9
  const [toasts, setToasts] = useState([]);
8
10
  const [container, setContainer] = useState();
9
11
  const [isStartedClosingNotify, setStartedClosingNotify] = useState(false);
@@ -29,7 +31,16 @@ export const useLogic = ({ containerId: externalContainerId, }) => {
29
31
  setStartedClosingNotify(false);
30
32
  }
31
33
  }, [toasts]);
32
- const { isHovered: isHoveredContainer } = useHover(container);
34
+ const hasOpenNotify = Boolean(toasts.length);
35
+ const { isHovered } = useHover(container, {
36
+ enabled: !isMobile,
37
+ });
38
+ const isTouchExpanded = useTouchStackExpand({
39
+ stackContainerElement: container ?? null,
40
+ enabled: isMobile,
41
+ hasOpenNotify,
42
+ });
43
+ const isStackExpanded = isMobile ? isTouchExpanded : isHovered;
33
44
  const handleAddToast = ({ id, containerId }) => {
34
45
  if (Object.is(containerId, externalContainerId)) {
35
46
  setToasts((currentToasts) => [...currentToasts, id]);
@@ -58,29 +69,28 @@ export const useLogic = ({ containerId: externalContainerId, }) => {
58
69
  return unsubscribe;
59
70
  }, []);
60
71
  useEffect(() => {
61
- if (!isHoveredContainer || !container) {
72
+ if (!isStackExpanded || !container) {
62
73
  return;
63
74
  }
64
75
  (async () => {
65
76
  // Ожидаем пока отработает анимация и стек с уведомлениями раскроется
66
77
  await sleep(300);
67
- const hasScroll = container?.scrollHeight > container?.clientHeight;
68
- if (hasScroll) {
78
+ const hasVerticalScroll = container.scrollHeight > container.clientHeight;
79
+ if (hasVerticalScroll) {
69
80
  container.scrollTo({
70
81
  top: container.scrollHeight,
71
82
  behavior: 'smooth',
72
83
  });
73
84
  }
74
85
  })();
75
- }, [container, isHoveredContainer]);
86
+ }, [container, isStackExpanded]);
76
87
  const closeAll = () => {
77
88
  setStartedClosingNotify(true);
78
89
  toast.dismiss({ containerId: externalContainerId });
79
90
  };
80
- const hasOpenNotify = Boolean(toasts.length);
81
91
  return {
82
- isVisibleCloseButton: hasOpenNotify,
83
- isHoveredContainer: isHoveredContainer && hasOpenNotify,
92
+ isVisibleCloseButton: hasOpenNotify && (isMobile ? isStackExpanded : true),
93
+ isStackExpanded,
84
94
  isStartedClosingNotify,
85
95
  closeAll,
86
96
  };
@@ -1,4 +1,6 @@
1
+ import { buttonClassnames } from '../../Button/constants';
1
2
  import { IconButton } from '../../IconButton';
3
+ import { svgIconClassnames } from '../../SvgIcon/constants';
2
4
  import { styled } from '../../styled';
3
5
  import { Typography } from '../../Typography';
4
6
  import { getActionsDirection } from './utils';
@@ -15,6 +17,10 @@ export const Wrapper = styled.article `
15
17
  color: ${({ theme }) => theme.palette.grey[900]};
16
18
 
17
19
  background: ${({ theme }) => theme.palette.background.default};
20
+
21
+ ${({ theme }) => theme.breakpoints.down('sm')} {
22
+ padding: ${({ theme }) => theme.spacing(2, 3)};
23
+ }
18
24
  `;
19
25
  export const Header = styled.header `
20
26
  display: flex;
@@ -22,6 +28,13 @@ export const Header = styled.header `
22
28
 
23
29
  width: 100%;
24
30
  min-height: 24px;
31
+
32
+ ${({ theme }) => theme.breakpoints.down('sm')} {
33
+ .${buttonClassnames.root} {
34
+ width: 24px;
35
+ height: 24px;
36
+ }
37
+ }
25
38
  `;
26
39
  export const Content = styled(Typography) `
27
40
  margin-top: ${({ theme }) => theme.spacing(2)};
@@ -38,6 +51,13 @@ export const IconWrapper = styled.div `
38
51
  display: flex;
39
52
 
40
53
  margin-right: ${({ theme }) => theme.spacing(2)};
54
+
55
+ ${({ theme }) => theme.breakpoints.down('sm')} {
56
+ .${svgIconClassnames.root} {
57
+ width: 24px;
58
+ height: 24px;
59
+ }
60
+ }
41
61
  `;
42
62
  export const CloseButton = styled(IconButton) `
43
63
  align-self: center;