@alfalab/core-components-gallery 5.2.13 → 5.3.1

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 (129) hide show
  1. package/Component-50136800.d.ts +38 -0
  2. package/Component.js +4 -4
  3. package/buttons-a2be6d97.d.ts +336 -0
  4. package/{buttons-a0cf7676.js → buttons-a2be6d97.js} +5 -5
  5. package/components/header/Component.js +2 -2
  6. package/components/header/buttons.js +2 -2
  7. package/components/header/index.css +6 -6
  8. package/components/header/index.js +2 -2
  9. package/components/header-info-block/Component.js +1 -1
  10. package/components/header-info-block/index.css +5 -5
  11. package/components/image-preview/Component.js +1 -1
  12. package/components/image-preview/index.css +13 -13
  13. package/components/image-viewer/component.js +1 -1
  14. package/components/image-viewer/index.css +19 -19
  15. package/components/image-viewer/index.js +1 -1
  16. package/components/image-viewer/slide.js +1 -1
  17. package/components/index.js +3 -3
  18. package/components/navigation-bar/Component.js +1 -1
  19. package/components/navigation-bar/index.css +6 -6
  20. package/cssm/Component-50136800.d.ts +38 -0
  21. package/cssm/Component.js +1 -1
  22. package/cssm/components/header/Component.js +1 -1
  23. package/cssm/components/header/buttons.js +4 -4
  24. package/cssm/components/header/index.js +1 -1
  25. package/cssm/components/index.js +1 -1
  26. package/cssm/index-50136800.d.ts +11 -0
  27. package/cssm/index-72dda473.d.ts +12 -0
  28. package/cssm/index-ebda875c.d.ts +35 -0
  29. package/cssm/index.js +1 -1
  30. package/cssm/types-83e2bd9e.d.ts +113 -0
  31. package/cssm/typings-9211a437.d.ts +95 -0
  32. package/esm/Component-50136800.d.ts +38 -0
  33. package/esm/Component.js +4 -4
  34. package/esm/buttons-83f36b01.d.ts +336 -0
  35. package/esm/{buttons-8138e150.js → buttons-83f36b01.js} +5 -5
  36. package/esm/components/header/Component.js +2 -2
  37. package/esm/components/header/buttons.js +2 -2
  38. package/esm/components/header/index.css +6 -6
  39. package/esm/components/header/index.js +2 -2
  40. package/esm/components/header-info-block/Component.js +1 -1
  41. package/esm/components/header-info-block/index.css +5 -5
  42. package/esm/components/image-preview/Component.js +1 -1
  43. package/esm/components/image-preview/index.css +13 -13
  44. package/esm/components/image-viewer/component.js +1 -1
  45. package/esm/components/image-viewer/index.css +19 -19
  46. package/esm/components/image-viewer/index.js +1 -1
  47. package/esm/components/image-viewer/slide.js +1 -1
  48. package/esm/components/index.js +3 -3
  49. package/esm/components/navigation-bar/Component.js +1 -1
  50. package/esm/components/navigation-bar/index.css +6 -6
  51. package/esm/index-50136800.d.ts +11 -0
  52. package/esm/index-72dda473.d.ts +12 -0
  53. package/esm/index-ebda875c.d.ts +35 -0
  54. package/esm/index.css +3 -3
  55. package/esm/index.js +3 -3
  56. package/esm/{slide-7040503f.js → slide-a481ee16.js} +1 -1
  57. package/esm/types-83e2bd9e.d.ts +113 -0
  58. package/esm/typings-9211a437.d.ts +95 -0
  59. package/index-50136800.d.ts +11 -0
  60. package/index-72dda473.d.ts +12 -0
  61. package/index-ebda875c.d.ts +35 -0
  62. package/index.css +3 -3
  63. package/index.js +3 -3
  64. package/modern/Component-50136800.d.ts +38 -0
  65. package/modern/Component.js +4 -4
  66. package/modern/buttons-fa6480fa.d.ts +336 -0
  67. package/modern/{buttons-c69fcb71.js → buttons-fa6480fa.js} +5 -5
  68. package/modern/components/header/Component.js +2 -2
  69. package/modern/components/header/buttons.js +2 -2
  70. package/modern/components/header/index.css +6 -6
  71. package/modern/components/header/index.js +2 -2
  72. package/modern/components/header-info-block/Component.js +1 -1
  73. package/modern/components/header-info-block/index.css +5 -5
  74. package/modern/components/image-preview/Component.js +1 -1
  75. package/modern/components/image-preview/index.css +13 -13
  76. package/modern/components/image-viewer/component.js +1 -1
  77. package/modern/components/image-viewer/index.css +19 -19
  78. package/modern/components/image-viewer/index.js +1 -1
  79. package/modern/components/image-viewer/slide.js +1 -1
  80. package/modern/components/index.js +3 -3
  81. package/modern/components/navigation-bar/Component.js +1 -1
  82. package/modern/components/navigation-bar/index.css +6 -6
  83. package/modern/index-50136800.d.ts +11 -0
  84. package/modern/index-72dda473.d.ts +12 -0
  85. package/modern/index-ebda875c.d.ts +35 -0
  86. package/modern/index.css +3 -3
  87. package/modern/index.js +3 -3
  88. package/modern/{slide-b249a536.js → slide-ef1f690b.js} +1 -1
  89. package/modern/types-83e2bd9e.d.ts +113 -0
  90. package/modern/typings-9211a437.d.ts +95 -0
  91. package/package.json +5 -5
  92. package/{slide-97bdc786.js → slide-610e04e2.js} +1 -1
  93. package/src/Component.tsx +185 -0
  94. package/src/components/header/Component.tsx +86 -0
  95. package/src/components/header/buttons.tsx +78 -0
  96. package/src/components/header/index.module.css +30 -0
  97. package/src/components/header/index.tsx +1 -0
  98. package/src/components/header-info-block/Component.tsx +47 -0
  99. package/src/components/header-info-block/index.module.css +29 -0
  100. package/src/components/header-info-block/index.ts +1 -0
  101. package/src/components/image-preview/Component.tsx +88 -0
  102. package/src/components/image-preview/index.module.css +68 -0
  103. package/src/components/image-preview/index.tsx +1 -0
  104. package/src/components/image-preview/paths.ts +5 -0
  105. package/src/components/image-viewer/component.tsx +232 -0
  106. package/src/components/image-viewer/index.module.css +126 -0
  107. package/src/components/image-viewer/index.ts +1 -0
  108. package/src/components/image-viewer/paths.ts +5 -0
  109. package/src/components/image-viewer/slide.tsx +113 -0
  110. package/src/components/index.ts +4 -0
  111. package/src/components/navigation-bar/Component.tsx +96 -0
  112. package/src/components/navigation-bar/index.module.css +31 -0
  113. package/src/components/navigation-bar/index.ts +1 -0
  114. package/src/context.ts +47 -0
  115. package/src/index.module.css +17 -0
  116. package/src/index.ts +2 -0
  117. package/src/types.ts +13 -0
  118. package/src/utils/constants.ts +10 -0
  119. package/src/utils/index.ts +3 -0
  120. package/src/utils/split-filename.ts +17 -0
  121. package/src/utils/utils.ts +18 -0
  122. package/types-83e2bd9e.d.ts +113 -0
  123. package/typings-9211a437.d.ts +95 -0
  124. package/buttons-a0cf7676.d.ts +0 -11
  125. package/esm/buttons-8138e150.d.ts +0 -11
  126. package/modern/buttons-c69fcb71.d.ts +0 -11
  127. /package/esm/{slide-7040503f.d.ts → slide-a481ee16.d.ts} +0 -0
  128. /package/modern/{slide-b249a536.d.ts → slide-ef1f690b.d.ts} +0 -0
  129. /package/{slide-97bdc786.d.ts → slide-610e04e2.d.ts} +0 -0
@@ -0,0 +1,232 @@
1
+ import React, {
2
+ FC,
3
+ KeyboardEventHandler,
4
+ MouseEventHandler,
5
+ SyntheticEvent,
6
+ useCallback,
7
+ useContext,
8
+ useEffect,
9
+ useMemo,
10
+ useRef,
11
+ } from 'react';
12
+ import cn from 'classnames';
13
+ import elementClosest from 'element-closest';
14
+ import SwiperCore, { A11y, Controller, EffectFade } from 'swiper';
15
+ import { Swiper, SwiperSlide } from 'swiper/react';
16
+
17
+ import { useFocus } from '@alfalab/hooks';
18
+ import { ChevronBackHeavyMIcon } from '@alfalab/icons-glyph/ChevronBackHeavyMIcon';
19
+ import { ChevronForwardHeavyMIcon } from '@alfalab/icons-glyph/ChevronForwardHeavyMIcon';
20
+
21
+ import { GalleryContext } from '../../context';
22
+ import { getImageAlt, getImageKey, TestIds } from '../../utils';
23
+
24
+ import { Slide } from './slide';
25
+
26
+ import 'swiper/swiper.min.css';
27
+ import styles from './index.module.css';
28
+
29
+ SwiperCore.use([EffectFade, A11y, Controller]);
30
+
31
+ export const ImageViewer: FC = () => {
32
+ const {
33
+ singleSlide,
34
+ images,
35
+ imagesMeta,
36
+ fullScreen,
37
+ currentSlideIndex,
38
+ initialSlide,
39
+ onClose,
40
+ getCurrentImage,
41
+ setImageMeta,
42
+ setCurrentSlideIndex,
43
+ getSwiper,
44
+ setSwiper,
45
+ slidePrev,
46
+ slideNext,
47
+ } = useContext(GalleryContext);
48
+
49
+ const leftArrowRef = useRef<HTMLDivElement>(null);
50
+ const rightArrowRef = useRef<HTMLDivElement>(null);
51
+
52
+ const [leftArrowFocused] = useFocus(leftArrowRef, 'keyboard');
53
+ const [rightArrowFocused] = useFocus(rightArrowRef, 'keyboard');
54
+
55
+ const swiper = getSwiper();
56
+
57
+ const handleSlideChange = useCallback(() => {
58
+ setCurrentSlideIndex(swiper?.activeIndex ?? initialSlide);
59
+ }, [setCurrentSlideIndex, swiper, initialSlide]);
60
+
61
+ const handlePrevClick = () => {
62
+ slidePrev();
63
+ };
64
+
65
+ const handleNextClick = () => {
66
+ slideNext();
67
+ };
68
+
69
+ const handleArrowLeftKeyDown: KeyboardEventHandler = (event) => {
70
+ if (event.key === 'Enter') {
71
+ slidePrev();
72
+ }
73
+ };
74
+
75
+ const handleArrowRightKeyDown: KeyboardEventHandler = (event) => {
76
+ if (event.key === 'Enter') {
77
+ slideNext();
78
+ }
79
+ };
80
+
81
+ const handleLoad = (event: SyntheticEvent<HTMLImageElement>, index: number) => {
82
+ const target = event.currentTarget;
83
+
84
+ const { naturalWidth, naturalHeight } = target;
85
+
86
+ setImageMeta({ width: naturalWidth, height: naturalHeight }, index);
87
+ };
88
+
89
+ const handleLoadError = (index: number) => {
90
+ setImageMeta({ width: 0, height: 0, broken: true }, index);
91
+ };
92
+
93
+ const handleWrapperClick = useCallback<MouseEventHandler>(
94
+ (event) => {
95
+ const eventTarget = event.target as HTMLElement;
96
+
97
+ const isArrow =
98
+ leftArrowRef.current?.contains(eventTarget) ||
99
+ rightArrowRef.current?.contains(eventTarget);
100
+
101
+ const isPlaceholder = Boolean(eventTarget.closest(`.${styles.placeholder}`));
102
+
103
+ const isImg = eventTarget.tagName === 'IMG';
104
+
105
+ if (!isImg && !isPlaceholder && !isArrow) {
106
+ onClose();
107
+ }
108
+ },
109
+ [onClose],
110
+ );
111
+
112
+ useEffect(() => {
113
+ elementClosest(window);
114
+ }, []);
115
+
116
+ const swiperProps = useMemo<Swiper>(
117
+ () => ({
118
+ slidesPerView: 1,
119
+ effect: 'fade',
120
+ fadeEffect: {
121
+ crossFade: true,
122
+ },
123
+ className: cn(styles.swiper, { [styles.hidden]: fullScreen }),
124
+ controller: { control: swiper },
125
+ a11y: {
126
+ slideRole: 'img',
127
+ },
128
+ initialSlide,
129
+ simulateTouch: false,
130
+ onSwiper: setSwiper,
131
+ onSlideChange: handleSlideChange,
132
+ }),
133
+ [swiper, fullScreen, initialSlide, handleSlideChange, setSwiper],
134
+ );
135
+
136
+ const showControls = !singleSlide && !fullScreen;
137
+
138
+ const swiperWidth = swiper?.width || 1;
139
+ const swiperHeight = swiper?.height || swiper?.width || 1;
140
+
141
+ const swiperAspectRatio = swiperWidth / swiperHeight;
142
+
143
+ const currentImage = getCurrentImage();
144
+
145
+ return (
146
+ /* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */
147
+ <div
148
+ className={cn(styles.component, { [styles.singleSlide]: singleSlide })}
149
+ onClick={handleWrapperClick}
150
+ >
151
+ {showControls && (
152
+ <div
153
+ className={cn(styles.arrow, {
154
+ [styles.focused]: leftArrowFocused,
155
+ })}
156
+ onClick={handlePrevClick}
157
+ role='button'
158
+ onKeyDown={handleArrowLeftKeyDown}
159
+ tabIndex={0}
160
+ ref={leftArrowRef}
161
+ aria-label='Предыдущее изображение'
162
+ data-test-id={TestIds.PREV_SLIDE_BUTTON}
163
+ >
164
+ <ChevronBackHeavyMIcon />
165
+ </div>
166
+ )}
167
+
168
+ {fullScreen && (
169
+ <img
170
+ src={currentImage?.src}
171
+ alt={currentImage ? getImageAlt(currentImage, currentSlideIndex) : ''}
172
+ className={styles.fullScreenImage}
173
+ />
174
+ )}
175
+
176
+ <Swiper {...swiperProps}>
177
+ {images.map((image, index) => {
178
+ const meta = imagesMeta[index];
179
+
180
+ const imageWidth = meta?.width || 1;
181
+ const imageHeight = meta?.height || 1;
182
+
183
+ const imageAspectRatio = imageWidth / imageHeight;
184
+
185
+ const slideVisible = index === currentSlideIndex;
186
+
187
+ return (
188
+ <SwiperSlide
189
+ key={getImageKey(image, index)}
190
+ style={{
191
+ pointerEvents: slideVisible ? 'auto' : 'none',
192
+ transitionProperty: 'opacity',
193
+ }}
194
+ >
195
+ {({ isActive }) => (
196
+ <Slide
197
+ isActive={isActive}
198
+ swiperAspectRatio={swiperAspectRatio}
199
+ image={image}
200
+ swiperHeight={swiperHeight}
201
+ meta={meta}
202
+ index={index}
203
+ imageAspectRatio={imageAspectRatio}
204
+ slideVisible={slideVisible}
205
+ handleLoad={handleLoad}
206
+ handleLoadError={handleLoadError}
207
+ />
208
+ )}
209
+ </SwiperSlide>
210
+ );
211
+ })}
212
+ </Swiper>
213
+
214
+ {showControls && (
215
+ <div
216
+ className={cn(styles.arrow, {
217
+ [styles.focused]: rightArrowFocused,
218
+ })}
219
+ onClick={handleNextClick}
220
+ role='button'
221
+ onKeyDown={handleArrowRightKeyDown}
222
+ tabIndex={0}
223
+ ref={rightArrowRef}
224
+ aria-label='Следующее изображение'
225
+ data-test-id={TestIds.NEXT_SLIDE_BUTTON}
226
+ >
227
+ <ChevronForwardHeavyMIcon />
228
+ </div>
229
+ )}
230
+ </div>
231
+ );
232
+ };
@@ -0,0 +1,126 @@
1
+ @import '@alfalab/core-components-themes/src/default.css';
2
+
3
+ .component {
4
+ display: flex;
5
+ flex-grow: 1;
6
+ justify-content: center;
7
+ align-items: center;
8
+ background-color: var(--color-static-bg-primary-dark);
9
+ }
10
+
11
+ .swiper {
12
+ display: flex;
13
+ width: 100%;
14
+ height: 100%;
15
+
16
+ /* 168px - высота хэдера и футера */
17
+ max-height: calc(100vh - 168px);
18
+ padding: var(--gap-2xl) var(--gap-m);
19
+ box-sizing: border-box;
20
+ }
21
+
22
+ .singleSlide .swiper {
23
+ max-height: calc(100vh - 80px);
24
+ padding: var(--gap-2xl);
25
+ }
26
+
27
+ .hidden {
28
+ display: none;
29
+ }
30
+
31
+ .slide {
32
+ position: relative;
33
+ display: flex;
34
+ justify-content: center;
35
+ align-items: center;
36
+ width: 100%;
37
+ height: 100%;
38
+ }
39
+
40
+ .slideLoading {
41
+ background-color: var(--color-static-bg-secondary-dark);
42
+ border-radius: var(--border-radius-m);
43
+ }
44
+
45
+ .image {
46
+ width: 0;
47
+ height: 0;
48
+ user-select: none;
49
+ background-color: var(--color-light-bg-primary);
50
+ border-radius: var(--border-radius-m);
51
+ }
52
+
53
+ .smallImage {
54
+ position: relative;
55
+ width: auto;
56
+ height: auto;
57
+ user-select: none;
58
+ background-color: var(--color-light-bg-primary);
59
+ }
60
+
61
+ .verticalImageFit {
62
+ width: auto;
63
+ height: 100%;
64
+ }
65
+
66
+ .horizontalImageFit {
67
+ width: 100%;
68
+ height: auto;
69
+ }
70
+
71
+ .arrow {
72
+ display: flex;
73
+ flex-direction: column;
74
+ justify-content: center;
75
+ align-items: center;
76
+ flex-shrink: 0;
77
+ width: 96px;
78
+ height: 100%;
79
+ cursor: pointer;
80
+ color: var(--color-static-graphic-light);
81
+ transition: background-color 0.15s ease-in-out;
82
+ outline: none;
83
+
84
+ &:hover {
85
+ background-color: var(--color-static-bg-primary-dark-tint-15);
86
+ }
87
+
88
+ &:active {
89
+ background-color: var(--color-static-bg-primary-dark-tint-20);
90
+ }
91
+ }
92
+
93
+ .focused {
94
+ @mixin focus-outline;
95
+ }
96
+
97
+ .placeholder {
98
+ display: flex;
99
+ justify-content: center;
100
+ align-items: center;
101
+ width: 400px;
102
+ height: 300px;
103
+ border-radius: var(--border-radius-m);
104
+ background-color: var(--color-static-bg-quaternary-dark);
105
+ }
106
+
107
+ .brokenImgWrapper {
108
+ position: relative;
109
+ display: flex;
110
+ flex-direction: column;
111
+ align-items: center;
112
+ width: 150px;
113
+ text-align: center;
114
+ }
115
+
116
+ .brokenImgIcon {
117
+ width: 80px;
118
+ height: 80px;
119
+ margin-bottom: var(--gap-2xs);
120
+ }
121
+
122
+ .fullScreenImage {
123
+ width: 100%;
124
+ height: auto;
125
+ background-color: var(--color-light-bg-primary);
126
+ }
@@ -0,0 +1 @@
1
+ export * from './component';
@@ -0,0 +1,5 @@
1
+ export const NoImagePaths = {
2
+ baseImage:
3
+ 'M61 13H8C5.79086 13 4 14.7908 4 17V63C4 65.2092 5.79086 67 8 67H72C74.2091 67 76 65.2092 76 63V27.9998H64.7333C62.6714 27.9998 61 26.3284 61 24.2665V13ZM23.8337 31.3334C24.57 30.597 25.7639 30.597 26.5003 31.3334L28.5001 33.3332L30.4999 31.3333C31.2363 30.5969 32.4302 30.5969 33.1666 31.3333C33.903 32.0697 33.903 33.2636 33.1666 34L31.1667 35.9998L33.1669 38C33.9033 38.7363 33.9033 39.9302 33.1669 40.6666C32.4306 41.403 31.2367 41.403 30.5003 40.6666L28.5001 38.6664L26.5 40.6666C25.7636 41.403 24.5697 41.403 23.8333 40.6666C23.097 39.9302 23.097 38.7363 23.8333 37.9999L25.8335 35.9998L23.8337 34C23.0973 33.2636 23.0973 32.0697 23.8337 31.3334ZM49.4984 31.3334C48.762 30.597 47.5681 30.597 46.8317 31.3334C46.0953 32.0698 46.0953 33.2637 46.8317 34L48.8315 35.9998L46.8314 38C46.095 38.7363 46.095 39.9302 46.8314 40.6666C47.5677 41.403 48.7617 41.403 49.498 40.6666L51.4982 38.6664L53.4984 40.6666C54.2347 41.403 55.4286 41.403 56.165 40.6666C56.9014 39.9303 56.9014 38.7364 56.165 38L54.1648 35.9998L56.1646 34C56.901 33.2636 56.901 32.0697 56.1646 31.3334C55.4283 30.597 54.2344 30.597 53.498 31.3334L51.4982 33.3332L49.4984 31.3334ZM28.8087 55.8292L28.8076 55.8297C27.7997 56.2743 26.6202 55.8199 26.1724 54.8123C25.7238 53.8029 26.1783 52.621 27.1877 52.1724L28 54C27.1877 52.1724 27.1889 52.1719 27.1889 52.1719L27.1902 52.1713L27.193 52.17L27.1999 52.167L27.2187 52.1588L27.2425 52.1486L27.2762 52.1343C27.3234 52.1144 27.388 52.0878 27.4698 52.0553C27.6333 51.9904 27.8654 51.9021 28.1633 51.7985C28.7588 51.5914 29.6177 51.3226 30.7163 51.0563C32.9132 50.5237 36.072 50 40 50C43.928 50 47.0868 50.5237 49.2837 51.0563C50.3823 51.3226 51.2412 51.5914 51.8367 51.7985C51.9387 51.834 52.033 51.8677 52.1195 51.8992C52.2855 51.9598 52.4227 52.0126 52.5302 52.0553C52.5704 52.0713 52.6065 52.0858 52.6384 52.0989C52.6713 52.1124 52.6998 52.1242 52.7238 52.1343L52.7677 52.1529L52.7813 52.1588L52.8001 52.167L52.807 52.17L52.8099 52.1713L52.8111 52.1719C52.8111 52.1719 52.8112 52.1719 52.8075 52.1806L52.7938 52.2121L52.8123 52.1724C53.8217 52.621 54.2762 53.8029 53.8276 54.8123C53.3798 55.8199 52.2013 56.2746 51.1931 55.83L51.1913 55.8292L40.8962 55.8291L51.1899 55.8286L51.1715 55.8208C51.149 55.8113 51.1097 55.795 51.0538 55.7728C50.9419 55.7284 50.7639 55.6604 50.5226 55.5765C50.0401 55.4086 49.3052 55.1774 48.3413 54.9437C46.4132 54.4763 43.572 54 40 54C36.428 54 33.5868 54.4763 31.6587 54.9437C30.6948 55.1774 29.9599 55.4086 29.4774 55.5765C29.2361 55.6604 29.0581 55.7284 28.9462 55.7728C28.8903 55.795 28.851 55.8113 28.8285 55.8208L28.8112 55.8281L40.8962 55.8291L28.8087 55.8292Z',
4
+ triangleImage: 'M75.9998 23.9998H65V13L75.9998 23.9998Z',
5
+ };
@@ -0,0 +1,113 @@
1
+ import React, { FC, ReactNode, SyntheticEvent } from 'react';
2
+ import cn from 'classnames';
3
+
4
+ import { Typography } from '@alfalab/core-components-typography';
5
+
6
+ import { GalleryImage, ImageMeta } from '../../types';
7
+ import { getImageAlt, isSmallImage, TestIds } from '../../utils';
8
+
9
+ import { NoImagePaths } from './paths';
10
+
11
+ import styles from './index.module.css';
12
+
13
+ type SlideInnerProps = {
14
+ active: boolean;
15
+ broken: boolean;
16
+ withPlaceholder: boolean;
17
+ loading: boolean;
18
+ children: ReactNode;
19
+ };
20
+
21
+ const SlideInner: FC<SlideInnerProps> = ({ children, broken, loading, withPlaceholder }) => {
22
+ const content = broken ? (
23
+ <div className={styles.brokenImgWrapper}>
24
+ <div className={styles.brokenImgIcon}>
25
+ <svg
26
+ xmlns='http://www.w3.org/2000/svg'
27
+ width='80'
28
+ height='80'
29
+ viewBox='0 0 80 80'
30
+ fill='none'
31
+ >
32
+ <rect width='80' height='80' fill='none' />
33
+ <path
34
+ fillRule='evenodd'
35
+ clipRule='evenodd'
36
+ d={NoImagePaths.baseImage}
37
+ fill='#89898A'
38
+ />
39
+ <path d={NoImagePaths.triangleImage} fill='#89898A' />
40
+ </svg>
41
+ </div>
42
+
43
+ <Typography.Text view='primary-small' color='static-secondary-light'>
44
+ Не удалось загрузить изображение
45
+ </Typography.Text>
46
+ </div>
47
+ ) : (
48
+ children
49
+ );
50
+
51
+ return (
52
+ <div className={cn(styles.slide, { [styles.slideLoading]: loading })}>
53
+ {withPlaceholder ? <div className={styles.placeholder}>{content}</div> : content}
54
+ </div>
55
+ );
56
+ };
57
+
58
+ type SlideProps = {
59
+ isActive: boolean;
60
+ image: GalleryImage;
61
+ meta?: ImageMeta;
62
+ swiperAspectRatio: number;
63
+ imageAspectRatio: number;
64
+ index: number;
65
+ swiperHeight: number;
66
+ slideVisible: boolean;
67
+ handleLoad: (event: SyntheticEvent<HTMLImageElement>, index: number) => void;
68
+ handleLoadError: (index: number) => void;
69
+ };
70
+
71
+ export const Slide: FC<SlideProps> = ({
72
+ isActive,
73
+ meta,
74
+ swiperAspectRatio,
75
+ imageAspectRatio,
76
+ image,
77
+ index,
78
+ swiperHeight,
79
+ slideVisible,
80
+ handleLoad,
81
+ handleLoadError,
82
+ }) => {
83
+ const broken = Boolean(meta?.broken);
84
+ const small = isSmallImage(meta);
85
+ const verticalImageFit = !small && swiperAspectRatio > imageAspectRatio;
86
+ const horizontalImageFit = !small && swiperAspectRatio <= imageAspectRatio;
87
+
88
+ return (
89
+ <SlideInner
90
+ active={isActive}
91
+ broken={broken}
92
+ loading={!meta}
93
+ withPlaceholder={small || broken}
94
+ >
95
+ <img
96
+ src={image.src}
97
+ alt={getImageAlt(image, index)}
98
+ className={cn({
99
+ [styles.smallImage]: small,
100
+ [styles.image]: !small,
101
+ [styles.verticalImageFit]: verticalImageFit,
102
+ [styles.horizontalImageFit]: horizontalImageFit,
103
+ })}
104
+ onLoad={(event) => handleLoad(event, index)}
105
+ onError={() => handleLoadError(index)}
106
+ style={{
107
+ maxHeight: `${swiperHeight}px`,
108
+ }}
109
+ data-test-id={slideVisible ? TestIds.ACTIVE_IMAGE : undefined}
110
+ />
111
+ </SlideInner>
112
+ );
113
+ };
@@ -0,0 +1,4 @@
1
+ export * from './navigation-bar';
2
+ export * from './header';
3
+ export * from './image-preview';
4
+ export * from './image-viewer';
@@ -0,0 +1,96 @@
1
+ import React, { FC, KeyboardEventHandler, useCallback, useContext, useEffect, useRef } from 'react';
2
+
3
+ import { GalleryContext } from '../../context';
4
+ import { getImageKey, TestIds } from '../../utils';
5
+ import { ImagePreview } from '../image-preview';
6
+
7
+ import styles from './index.module.css';
8
+
9
+ const MIN_SCROLL_STEP = 24;
10
+
11
+ export const NavigationBar: FC = () => {
12
+ const containerRef = useRef<HTMLDivElement>(null);
13
+
14
+ const { images, currentSlideIndex, setCurrentSlideIndex, getSwiper } =
15
+ useContext(GalleryContext);
16
+
17
+ const swiper = getSwiper();
18
+
19
+ const handlePreviewSelect = (index: number) => {
20
+ setCurrentSlideIndex(index);
21
+
22
+ if (swiper) {
23
+ swiper.slideTo(index);
24
+ }
25
+ };
26
+
27
+ const scroll = useCallback((scrollValue: number) => {
28
+ if (containerRef.current) {
29
+ containerRef.current.scroll({
30
+ top: 0,
31
+ left: containerRef.current.scrollLeft + scrollValue,
32
+ behavior: 'smooth',
33
+ });
34
+ }
35
+ }, []);
36
+
37
+ const handlePreviewPosition = useCallback(
38
+ (preview: Element, containerWidth: number) => {
39
+ const { right, left } = preview.getBoundingClientRect();
40
+
41
+ if (right > containerWidth) {
42
+ const scrollValue = right - containerWidth + MIN_SCROLL_STEP;
43
+
44
+ scroll(scrollValue);
45
+ } else if (left < 0) {
46
+ const scrollValue = left - MIN_SCROLL_STEP;
47
+
48
+ scroll(scrollValue);
49
+ }
50
+ },
51
+ [scroll],
52
+ );
53
+
54
+ const handleKeyDown: KeyboardEventHandler = (event) => {
55
+ if (['ArrowLeft', 'ArrowRight'].includes(event.key)) {
56
+ event.preventDefault();
57
+ }
58
+ };
59
+
60
+ useEffect(() => {
61
+ if (containerRef.current) {
62
+ const { width: containerWidth } = containerRef.current.getBoundingClientRect();
63
+
64
+ const activePreview = containerRef.current.children[currentSlideIndex];
65
+
66
+ if (activePreview) {
67
+ handlePreviewPosition(activePreview, containerWidth);
68
+ }
69
+ }
70
+ }, [currentSlideIndex, handlePreviewPosition, scroll]);
71
+
72
+ return (
73
+ // eslint-disable-next-line jsx-a11y/no-static-element-interactions
74
+ <div
75
+ className={styles.component}
76
+ ref={containerRef}
77
+ onKeyDown={handleKeyDown}
78
+ data-test-id={TestIds.NAVIGATION_BAR}
79
+ >
80
+ {images.map((image, index) => {
81
+ const active = index === currentSlideIndex;
82
+
83
+ return (
84
+ <ImagePreview
85
+ key={getImageKey(image, index)}
86
+ image={image}
87
+ active={active}
88
+ index={index}
89
+ onSelect={handlePreviewSelect}
90
+ className={styles.preview}
91
+ />
92
+ );
93
+ })}
94
+ </div>
95
+ );
96
+ };
@@ -0,0 +1,31 @@
1
+ @import '@alfalab/core-components-themes/src/default.css';
2
+
3
+ .component {
4
+ display: flex;
5
+ flex-wrap: nowrap;
6
+ align-content: center;
7
+ align-items: center;
8
+ flex-shrink: 0;
9
+ overflow-x: auto;
10
+ box-sizing: border-box;
11
+ padding: 10px var(--gap-xl);
12
+ -ms-overflow-style: none;
13
+ scrollbar-width: none;
14
+
15
+ &::-webkit-scrollbar {
16
+ display: none;
17
+ }
18
+ }
19
+
20
+ .preview {
21
+ flex-shrink: 0;
22
+ margin: 0 var(--gap-3xs);
23
+
24
+ &:first-child {
25
+ margin-left: auto;
26
+ }
27
+
28
+ &:last-child {
29
+ margin-right: auto;
30
+ }
31
+ }
@@ -0,0 +1 @@
1
+ export * from './Component';
package/src/context.ts ADDED
@@ -0,0 +1,47 @@
1
+ import { createContext } from 'react';
2
+ import SwiperCore from 'swiper';
3
+
4
+ import { GalleryImage, ImageMeta } from './types';
5
+
6
+ export type GalleryContext = {
7
+ singleSlide: boolean;
8
+ currentSlideIndex: number;
9
+ images: GalleryImage[];
10
+ imagesMeta: ImageMeta[];
11
+ fullScreen: boolean;
12
+ initialSlide: number;
13
+ setFullScreen: (fullScreen: boolean) => void;
14
+ setImageMeta: (meta: ImageMeta, index: number) => void;
15
+ slideTo: (index: number) => void;
16
+ slideNext: () => void;
17
+ slidePrev: () => void;
18
+ getSwiper: () => SwiperCore | undefined;
19
+ setSwiper: (swiper: SwiperCore) => void;
20
+ onClose: () => void;
21
+ setCurrentSlideIndex: (index: number) => void;
22
+ getCurrentImage: () => GalleryImage | undefined;
23
+ getCurrentImageMeta: () => ImageMeta | undefined;
24
+ };
25
+
26
+ const mockFn = () => undefined;
27
+
28
+ // eslint-disable-next-line @typescript-eslint/no-redeclare
29
+ export const GalleryContext = createContext<GalleryContext>({
30
+ singleSlide: false,
31
+ currentSlideIndex: 0,
32
+ images: [],
33
+ imagesMeta: [],
34
+ fullScreen: false,
35
+ initialSlide: 0,
36
+ setFullScreen: mockFn,
37
+ setImageMeta: mockFn,
38
+ slideTo: mockFn,
39
+ slideNext: mockFn,
40
+ slidePrev: mockFn,
41
+ getSwiper: mockFn,
42
+ setSwiper: mockFn,
43
+ onClose: mockFn,
44
+ setCurrentSlideIndex: mockFn,
45
+ getCurrentImage: mockFn,
46
+ getCurrentImageMeta: mockFn,
47
+ });
@@ -0,0 +1,17 @@
1
+ @import '@alfalab/core-components-themes/src/default.css';
2
+
3
+ .container {
4
+ display: flex;
5
+ flex-direction: column;
6
+ justify-content: space-between;
7
+ height: 100%;
8
+ width: 100%;
9
+ background-color: var(--color-static-bg-primary-dark);
10
+ }
11
+
12
+ .modal {
13
+ flex-grow: 1;
14
+ width: 100%;
15
+ height: 100%;
16
+ background: transparent;
17
+ }
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * from './Component';
2
+ export * from './utils';