playbook_ui 10.21.0.pre.alpha.jg1 → 10.21.0.pre.alpha.lightbox

Sign up to get free protection for your applications and to get access to all the features.
Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/app/pb_kits/playbook/_playbook.scss +1 -0
  3. data/app/pb_kits/playbook/data/menu.yml +1 -0
  4. data/app/pb_kits/playbook/index.js +2 -1
  5. data/app/pb_kits/playbook/pb_avatar/_avatar.jsx +1 -1
  6. data/app/pb_kits/playbook/pb_avatar/_avatar.scss +2 -2
  7. data/app/pb_kits/playbook/pb_avatar/avatar.rb +1 -1
  8. data/app/pb_kits/playbook/pb_avatar/avatar.test.js +1 -1
  9. data/app/pb_kits/playbook/pb_background/_background.jsx +2 -7
  10. data/app/pb_kits/playbook/pb_background/_background.scss +8 -24
  11. data/app/pb_kits/playbook/pb_background/background.rb +6 -6
  12. data/app/pb_kits/playbook/pb_background/docs/_background_image.html.erb +2 -50
  13. data/app/pb_kits/playbook/pb_background/docs/_background_image.jsx +28 -71
  14. data/app/pb_kits/playbook/pb_bar_graph/_bar_graph.jsx +4 -0
  15. data/app/pb_kits/playbook/pb_bar_graph/bar_graph.rb +1 -0
  16. data/app/pb_kits/playbook/pb_button/_button.jsx +3 -3
  17. data/app/pb_kits/playbook/pb_button/_button.scss +18 -1
  18. data/app/pb_kits/playbook/pb_button/button.rb +11 -3
  19. data/app/pb_kits/playbook/pb_button/button.test.js +13 -0
  20. data/app/pb_kits/playbook/pb_button/docs/_button_size.html.erb +3 -0
  21. data/app/pb_kits/playbook/pb_button/docs/_button_size.jsx +26 -0
  22. data/app/pb_kits/playbook/pb_button/docs/_button_size.md +1 -0
  23. data/app/pb_kits/playbook/pb_button/docs/example.yml +2 -0
  24. data/app/pb_kits/playbook/pb_button/docs/index.js +1 -0
  25. data/app/pb_kits/playbook/pb_circle_chart/_circle_chart.jsx +3 -0
  26. data/app/pb_kits/playbook/pb_circle_chart/circle_chart.rb +1 -0
  27. data/app/pb_kits/playbook/pb_dashboard/pbChartsDarkTheme.js +215 -0
  28. data/app/pb_kits/playbook/pb_file_upload/_file_upload.jsx +17 -10
  29. data/app/pb_kits/playbook/pb_file_upload/fileupload.test.js +40 -0
  30. data/app/pb_kits/playbook/pb_gauge/_gauge.jsx +3 -0
  31. data/app/pb_kits/playbook/pb_gauge/gauge.rb +1 -0
  32. data/app/pb_kits/playbook/pb_image/_image.jsx +1 -1
  33. data/app/pb_kits/playbook/pb_image/_image.scss +3 -3
  34. data/app/pb_kits/playbook/pb_image/image.rb +1 -1
  35. data/app/pb_kits/playbook/pb_image/image.test.js +1 -1
  36. data/app/pb_kits/playbook/pb_lightbox/Carousel/Slide.jsx +55 -0
  37. data/app/pb_kits/playbook/pb_lightbox/Carousel/Slides.jsx +54 -0
  38. data/app/pb_kits/playbook/pb_lightbox/Carousel/Thumbnail.jsx +39 -0
  39. data/app/pb_kits/playbook/pb_lightbox/Carousel/Thumbnails.jsx +82 -0
  40. data/app/pb_kits/playbook/pb_lightbox/Carousel/index.jsx +59 -0
  41. data/app/pb_kits/playbook/pb_lightbox/Carousel/styles.scss +110 -0
  42. data/app/pb_kits/playbook/pb_lightbox/Carousel/useSlides.js +66 -0
  43. data/app/pb_kits/playbook/pb_lightbox/_lightbox.jsx +112 -0
  44. data/app/pb_kits/playbook/pb_lightbox/_lightbox_context.jsx +3 -0
  45. data/app/pb_kits/playbook/pb_lightbox/_lightbox_header.jsx +71 -0
  46. data/app/pb_kits/playbook/pb_lightbox/_lightbox_header_icon.jsx +26 -0
  47. data/app/pb_kits/playbook/pb_lightbox/docs/_lightbox_compound_component.jsx +95 -0
  48. data/app/pb_kits/playbook/pb_lightbox/docs/_lightbox_default.jsx +64 -0
  49. data/app/pb_kits/playbook/pb_lightbox/docs/_lightbox_default.md +1 -0
  50. data/app/pb_kits/playbook/pb_lightbox/docs/_lightbox_multiple.jsx +64 -0
  51. data/app/pb_kits/playbook/pb_lightbox/docs/example.yml +7 -0
  52. data/app/pb_kits/playbook/pb_lightbox/docs/index.js +3 -0
  53. data/app/pb_kits/playbook/pb_lightbox/hooks/useVisibility.js +21 -0
  54. data/app/pb_kits/playbook/pb_lightbox/hooks/useWindowSize.js +25 -0
  55. data/app/pb_kits/playbook/pb_lightbox/lightbox.scss +90 -0
  56. data/app/pb_kits/playbook/pb_lightbox/lightbox.test.jsx +30 -0
  57. data/app/pb_kits/playbook/pb_line_graph/_line_graph.jsx +4 -0
  58. data/app/pb_kits/playbook/pb_line_graph/line_graph.rb +1 -0
  59. data/app/pb_kits/playbook/pb_popover/_popover.jsx +2 -4
  60. data/app/pb_kits/playbook/pb_popover/docs/_popover_close.html.erb +7 -7
  61. data/app/pb_kits/playbook/pb_popover/index.js +4 -9
  62. data/app/pb_kits/playbook/pb_popover/popover.html.erb +1 -1
  63. data/app/pb_kits/playbook/pb_rich_text_editor/_rich_text_editor.jsx +4 -0
  64. data/app/pb_kits/playbook/pb_text_input/_text_input.scss +2 -2
  65. data/app/pb_kits/playbook/pb_text_input/text_input.test.js +14 -0
  66. data/app/pb_kits/playbook/playbook-doc.js +2 -0
  67. data/app/pb_kits/playbook/plugins/pb_chart.js +8 -4
  68. data/lib/playbook/version.rb +2 -2
  69. metadata +28 -3
  70. data/app/pb_kits/playbook/pb_background/docs/_background_image.md +0 -1
@@ -0,0 +1,215 @@
1
+ import colors from '../tokens/exports/_colors.scss'
2
+ import typography from '../tokens/exports/_typography.scss'
3
+
4
+ import Highcharts from 'highcharts'
5
+
6
+ const highchartsDarkTheme = {
7
+ lang: {
8
+ thousandsSep: ',',
9
+ },
10
+ colors: [
11
+ colors.data_1,
12
+ colors.data_2,
13
+ colors.data_3,
14
+ colors.data_4,
15
+ colors.data_5,
16
+ colors.data_6,
17
+ colors.data_7,
18
+ ],
19
+ chart: {
20
+ borderWidth: 0,
21
+ borderRadius: 0,
22
+ plotBackgroundColor: null,
23
+ plotShadow: false,
24
+ plotBorderWidth: 0,
25
+ },
26
+ title: {
27
+ style: {
28
+ color: colors.text_dk_default,
29
+ fontFamily: typography.font_family_base,
30
+ fontWeight: typography.regular,
31
+ fontSize: typography.heading_3,
32
+ },
33
+ },
34
+ subtitle: {
35
+ style: {
36
+ fontFamily: typography.font_family_base,
37
+ color: colors.text_dk_light,
38
+ fontWeight: typography.regular,
39
+ fontSize: typography.text_base,
40
+ },
41
+ },
42
+ xAxis: {
43
+ gridLineWidth: 0,
44
+ lineColor: colors.border_dark,
45
+ tickColor: colors.border_dark,
46
+ labels: {
47
+ style: {
48
+ fontFamily: typography.font_family_base,
49
+ color: colors.text_dk_lighter,
50
+ fontWeight: typography.bold,
51
+ fontSize: typography.text_smaller,
52
+ },
53
+ },
54
+ title: {
55
+ style: {
56
+ color: colors.text_dk_default,
57
+ fontFamily: typography.font_family_base,
58
+ fontWeight: typography.regular,
59
+ fontSize: typography.heading_4,
60
+ },
61
+ },
62
+ },
63
+ yAxis: {
64
+ alternateGridColor: null,
65
+ minorTickInterval: null,
66
+ gridLineColor: colors.border_dark,
67
+ minorGridLineColor: colors.border_dark,
68
+ lineWidth: 0,
69
+ tickWidth: 0,
70
+ labels: {
71
+ style: {
72
+ fontFamily: typography.font_family_base,
73
+ color: colors.text_dk_lighter,
74
+ fontWeight: typography.bold,
75
+ fontSize: typography.text_smaller,
76
+ },
77
+ },
78
+ title: {
79
+ style: {
80
+ fontFamily: typography.font_family_base,
81
+ color: colors.text_dk_lighter,
82
+ fontWeight: typography.bold,
83
+ fontSize: typography.text_smaller,
84
+ },
85
+ },
86
+ },
87
+ legend: {
88
+ layout: 'horizontal',
89
+ align: 'center',
90
+ verticalAlign: 'bottom',
91
+ itemStyle: {
92
+ fontFamily: typography.font_family_base,
93
+ color: colors.text_dk_light,
94
+ fontWeight: typography.regular,
95
+ fontSize: typography.text_smaller,
96
+ },
97
+ itemHoverStyle: {
98
+ color: colors.text_dk_default,
99
+ },
100
+ itemHiddenStyle: {
101
+ color: colors.text_dk_lighter,
102
+ },
103
+ },
104
+ labels: {
105
+ style: {
106
+ color: colors.primary,
107
+ },
108
+ },
109
+ tooltip: {
110
+ backgroundColor: {
111
+ linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 },
112
+ stops: [
113
+ [0, colors.card_light],
114
+ [1, colors.card_light],
115
+ ],
116
+ },
117
+ shadow: false,
118
+ borderWidth: 0,
119
+ borderRadius: 10,
120
+ style: {
121
+ fontFamily: typography.font_family_base,
122
+ color: colors.text_lt_default,
123
+ fontWeight: typography.regular,
124
+ fontSize: typography.text_smaller,
125
+ },
126
+ },
127
+ // specific to gauge
128
+ // unfilled gauge color
129
+ pane: {
130
+ background: {
131
+ borderColor: colors.border_dark,
132
+ },
133
+ },
134
+
135
+ plotOptions: {
136
+ series: {
137
+ type: 'area',
138
+ nullColor: colors.text_dk_lighter,
139
+ fillColor: {
140
+ linearGradient: {
141
+ x1: 0,
142
+ y1: 0,
143
+ x2: 0,
144
+ y2: 1,
145
+ },
146
+ stops: [
147
+ [0, Highcharts.getOptions().colors[0]],
148
+ [1, 'white'],
149
+ ],
150
+ },
151
+ threshold: null,
152
+ },
153
+
154
+ // GAUGE STYLES
155
+ solidgauge: {
156
+ borderColor: colors.primary,
157
+ borderWidth: 20,
158
+ radius: 90,
159
+ innerRadius: '90%',
160
+ dataLabels: {
161
+ borderWidth: 0,
162
+ color: colors.text_dk_default,
163
+ enabled: true,
164
+ style: {
165
+ fontFamily: typography.font_family_base,
166
+ fontWeight: typography.regular,
167
+ fontSize: typography.heading_2,
168
+ },
169
+ y: -26,
170
+ },
171
+ },
172
+
173
+ // PIE STYLES
174
+ pie: {
175
+ colors: [
176
+ colors.data_1,
177
+ colors.data_2,
178
+ colors.data_3,
179
+ colors.data_4,
180
+ colors.data_5,
181
+ colors.data_6,
182
+ colors.data_7,
183
+ ],
184
+ dataLabels: {
185
+ style: {
186
+ fontFamily: typography.font_family_base,
187
+ fontSize: typography.text_smaller,
188
+ color: colors.text_dk_light,
189
+ fontWeight: typography.regular,
190
+ },
191
+ },
192
+ },
193
+
194
+ // LINE CHART STYLES
195
+ line: {
196
+ dataLabels: {
197
+ color: colors.text_dk_light,
198
+ },
199
+ marker: {
200
+ lineColor: colors.border_dark,
201
+ },
202
+ area: {
203
+ shadow: false,
204
+ states: {
205
+ hover: {
206
+ lineWidth: 1,
207
+ },
208
+ },
209
+ threshold: null,
210
+ },
211
+ },
212
+ },
213
+ }
214
+
215
+ export { highchartsDarkTheme }
@@ -4,7 +4,7 @@ import React, { useCallback } from 'react'
4
4
  import { useDropzone } from 'react-dropzone'
5
5
  import classnames from 'classnames'
6
6
 
7
- import { buildCss, noop } from '../utilities/props'
7
+ import { buildCss, buildDataProps, noop } from '../utilities/props'
8
8
  import { globalProps } from '../utilities/globalProps'
9
9
  import type { Callback } from '../types'
10
10
 
@@ -14,15 +14,17 @@ import Card from '../pb_card/_card'
14
14
  type FileUploadProps = {
15
15
  accept?: array<string>,
16
16
  className?: string,
17
+ data?: object,
17
18
  acceptedFilesDescription?: string,
18
19
  onFilesAccepted: Callback,
19
20
  }
20
21
 
21
22
  const FileUpload = (props: FileUploadProps) => {
22
23
  const {
23
- accept = ['*'],
24
+ accept = null,
24
25
  acceptedFilesDescription = '',
25
26
  className,
27
+ data = {},
26
28
  onFilesAccepted = noop,
27
29
  } = props
28
30
  const onDrop = useCallback((files) => {
@@ -34,17 +36,22 @@ const FileUpload = (props: FileUploadProps) => {
34
36
  onDrop,
35
37
  })
36
38
 
37
- const acceptedFileTypes = accept.map((fileType) => {
38
- if (fileType.startsWith('image/')) {
39
- return fileType.replace('image/', ' ')
40
- } else {
41
- return fileType
42
- }
43
- })
39
+ const acceptedFileTypes = () => {
40
+ return accept.map((fileType) => {
41
+ if (fileType.startsWith('image/')) {
42
+ return fileType.replace('image/', ' ')
43
+ } else {
44
+ return fileType
45
+ }
46
+ })
47
+ }
48
+
49
+ const dataProps = buildDataProps(data)
44
50
 
45
51
  return (
46
52
  <div
47
53
  className={classnames(buildCss('pb_file_upload_kit'), globalProps(props), className)}
54
+ {...dataProps}
48
55
  {...getRootProps()}
49
56
  >
50
57
  <Card>
@@ -53,7 +60,7 @@ const FileUpload = (props: FileUploadProps) => {
53
60
  <If condition={isDragActive}>
54
61
  <p>{'Drop the files here ...'}</p>
55
62
  <Else />
56
- <p>{`Choose a file or drag it here. The accepted file types are: ${acceptedFilesDescription || acceptedFileTypes}`}</p>
63
+ <p>{accept === null ? 'Choose a file or drag it here' : `Choose a file or drag it here. The accepted file types are: ${acceptedFilesDescription || acceptedFileTypes()}`}</p>
57
64
  </If>
58
65
  </Body>
59
66
  </Card>
@@ -0,0 +1,40 @@
1
+ import React from 'react'
2
+ import { render, screen } from '../utilities/test-utils'
3
+
4
+ import FileUpload from './_file_upload'
5
+
6
+ const testid = 'fileupload-test'
7
+
8
+ test('returns namespaced class name', () => {
9
+ render(
10
+ <FileUpload
11
+ data={{ testid: testid }}
12
+ />
13
+ )
14
+
15
+ const kit = screen.getByTestId(testid)
16
+ expect(kit).toHaveClass('pb_file_upload_kit')
17
+ })
18
+
19
+ test('shows default drag text', () => {
20
+ render(
21
+ <FileUpload
22
+ data={{ testid: testid }}
23
+ />
24
+ )
25
+
26
+ const kit = screen.getByTestId(testid)
27
+ expect(kit).toHaveTextContent('Choose a file or drag it here')
28
+ })
29
+
30
+ test('shows type-specific drag text', () => {
31
+ render(
32
+ <FileUpload
33
+ accept={['image/svg+xml']}
34
+ data={{ testid: testid }}
35
+ />
36
+ )
37
+
38
+ const kit = screen.getByTestId(testid)
39
+ expect(kit).toHaveTextContent('Choose a file or drag it here. The accepted file types are: svg+xml')
40
+ })
@@ -12,6 +12,7 @@ type GaugeProps = {
12
12
  aria: Object,
13
13
  className?: string,
14
14
  chartData?: array,
15
+ dark?: Boolean,
15
16
  data?: Object,
16
17
  disableAnimation: boolean,
17
18
  fullCircle: boolean,
@@ -33,6 +34,7 @@ const Gauge = (props: GaugeProps) => {
33
34
  aria = {},
34
35
  className,
35
36
  chartData = [{ name: 'Name', value: 0 }],
37
+ dark = false,
36
38
  data = {},
37
39
  disableAnimation = false,
38
40
  fullCircle = false,
@@ -67,6 +69,7 @@ const Gauge = (props: GaugeProps) => {
67
69
  id: id,
68
70
  chartData: formattedChartData,
69
71
  circumference: fullCircle ? [0, 360] : [-100, 100],
72
+ dark,
70
73
  disableAnimation: disableAnimation,
71
74
  height: height,
72
75
  min: min,
@@ -32,6 +32,7 @@ module Playbook
32
32
  id: id,
33
33
  chartData: chart_data_formatted,
34
34
  circumference: full_circle ? [0, 360] : [-100, 100],
35
+ dark: dark ? "dark" : "",
35
36
  disableAnimation: disable_animation,
36
37
  height: height,
37
38
  min: min,
@@ -34,7 +34,7 @@ const Image = (props: ImageProps) => {
34
34
 
35
35
  const ariaProps = buildAriaProps(aria)
36
36
  const classes = classnames(
37
- buildCss('pb_image_kit', size),
37
+ buildCss('pb_image_kit', size ? `size_${size}` : null),
38
38
  'lazyload',
39
39
  transition,
40
40
  { rounded },
@@ -14,7 +14,7 @@ $image-sizes: (
14
14
  object-fit: cover;
15
15
 
16
16
  @each $name, $size in $image-sizes {
17
- &[class*=_#{$name}] {
17
+ &[class*=size_#{$name}] {
18
18
  width: $size;
19
19
  height: $size;
20
20
  object-fit: cover;
@@ -35,7 +35,7 @@ $image-sizes: (
35
35
  transition: opacity 300ms ease-in;
36
36
  }
37
37
  }
38
-
38
+
39
39
  &.blur {
40
40
  filter: blur(5px);
41
41
  &.lazyloaded {
@@ -44,7 +44,7 @@ $image-sizes: (
44
44
  transition: filter 300ms ease-in;
45
45
  }
46
46
  }
47
-
47
+
48
48
  &.scale {
49
49
  opacity: 0;
50
50
  transform: scale(0.9);
@@ -27,7 +27,7 @@ module Playbook
27
27
  end
28
28
 
29
29
  def size_class
30
- size == "none" ? nil : "_#{size}"
30
+ size == "none" ? nil : "_size_#{size}"
31
31
  end
32
32
 
33
33
  def transition_class
@@ -26,7 +26,7 @@ test('default classname', () => {
26
26
 
27
27
  test('size = xs', () => {
28
28
  const kit = renderKit(Image, props, { size: 'xs' })
29
- expect(kit).toHaveClass('pb_image_kit_xs lazyload')
29
+ expect(kit).toHaveClass('pb_image_kit_size_xs lazyload')
30
30
  })
31
31
 
32
32
  test('transition = blur', () => {
@@ -0,0 +1,55 @@
1
+ /* @flow */
2
+
3
+ import React from 'react'
4
+ import { noop } from 'lodash'
5
+ import { TransformComponent, TransformWrapper } from 'react-zoom-pan-pinch'
6
+ import Image from '../../pb_image/_image'
7
+
8
+ import styles from './styles.scss'
9
+
10
+ type SlideType = {
11
+ alt: string,
12
+ current: number,
13
+ onClick: () => void,
14
+ onChange: (index: number) => void,
15
+ onZoom: (zoom: number) => void,
16
+ zooming: boolean,
17
+ url: string,
18
+ }
19
+
20
+ export default function Slide({
21
+ alt,
22
+ onClick = noop,
23
+ onZoom = noop,
24
+ url,
25
+ zooming = false,
26
+ }: SlideType) {
27
+ const handlePinchingStop = (e) => {
28
+ const isZooming = e.state.scale > 1
29
+ onZoom(isZooming)
30
+ }
31
+
32
+ return (
33
+ <TransformWrapper
34
+ doubleClick={{ mode: 'reset' }}
35
+ initialScale={1}
36
+ onPinchingStop={handlePinchingStop}
37
+ panning={{ disabled: !zooming }}
38
+ >
39
+ <button
40
+ className={styles.Slide}
41
+ onClick={onClick}
42
+ onDoubleClick={() => onZoom(false)}
43
+ tabIndex={-1}
44
+ >
45
+ <TransformComponent className={styles.TransformComponent}>
46
+ <Image
47
+ alt={alt}
48
+ url={url}
49
+ zIndex="3"
50
+ />
51
+ </TransformComponent>
52
+ </button>
53
+ </TransformWrapper>
54
+ )
55
+ }
@@ -0,0 +1,54 @@
1
+ /* @flow */
2
+
3
+ import { noop } from 'lodash'
4
+ import { motion } from 'framer-motion'
5
+ import React, { useState } from 'react'
6
+
7
+ import Slide from './Slide'
8
+ import styles from './styles.scss'
9
+ import useSlides from './useSlides'
10
+
11
+ type SlidesType = {
12
+ urls: Array<string>,
13
+ current: number,
14
+ onChange: (index: number) => void,
15
+ onClick: (index: number) => void,
16
+ }
17
+
18
+ export default function Slides({
19
+ urls = [],
20
+ current = 0,
21
+ onClick = noop,
22
+ onChange = noop,
23
+ }: SlidesType) {
24
+ const [zooming, setZooming] = useState(false)
25
+ const { controls, dragConstraints, handleDragEnd } = useSlides({
26
+ current,
27
+ pagesCount: urls.length,
28
+ onChange,
29
+ })
30
+
31
+ const handleZoom = (isZooming) => setZooming(isZooming)
32
+
33
+ return (
34
+ <motion.div
35
+ animate={controls}
36
+ className={styles.Slides}
37
+ drag={!zooming && 'x'}
38
+ dragConstraints={dragConstraints}
39
+ dragElastic={0.05}
40
+ onDragEnd={handleDragEnd}
41
+ transition={{ type: 'spring', bounce: 0 }}
42
+ >
43
+ {urls.map((url, i) => (
44
+ <Slide
45
+ key={i}
46
+ onClick={() => onClick(i)}
47
+ onZoom={handleZoom}
48
+ url={url}
49
+ zooming={zooming}
50
+ />
51
+ ))}
52
+ </motion.div>
53
+ )
54
+ }
@@ -0,0 +1,39 @@
1
+ /* @flow */
2
+
3
+ import React from 'react'
4
+ import { noop } from 'lodash'
5
+ import classnames from 'classnames'
6
+ import Image from '../../pb_image/_image'
7
+
8
+ import styles from './styles.scss'
9
+
10
+ type ThumbnailType = {
11
+ active?: boolean,
12
+ alt?: string,
13
+ onClick: () => void,
14
+ url: string,
15
+ width?: string,
16
+ }
17
+
18
+ export default function Thumbnail({
19
+ active = false,
20
+ alt,
21
+ width,
22
+ url,
23
+ onClick = noop,
24
+ }: ThumbnailType) {
25
+ return (
26
+ <button
27
+ className={classnames(styles.Thumbnail, { [styles.active]: active })}
28
+ onClick={onClick}
29
+ style={{ width }}
30
+ type="button"
31
+ >
32
+ <Image
33
+ alt={alt}
34
+ size="sm"
35
+ url={url}
36
+ />
37
+ </button>
38
+ )
39
+ }
@@ -0,0 +1,82 @@
1
+ /* @flow */
2
+
3
+ import { noop } from 'lodash'
4
+ import classnames from 'classnames'
5
+ import React, { useEffect } from 'react'
6
+ import { motion, useAnimation } from 'framer-motion'
7
+ import { useWindowSize } from '../hooks/useWindowSize'
8
+
9
+ import Thumbnail from './Thumbnail'
10
+
11
+ import styles from './styles.scss'
12
+
13
+ export const indexWithinBounds = (
14
+ current: number,
15
+ min: number,
16
+ max: number
17
+ ): number => {
18
+ if (current < min) return 0
19
+ if (current > max) return max
20
+ return current
21
+ }
22
+
23
+ type ThumbnailsType = {
24
+ current: number,
25
+ onChange: () => null,
26
+ urls: [],
27
+ }
28
+
29
+ export default function Thumbnails({
30
+ current = 0,
31
+ onChange = noop,
32
+ urls = [],
33
+ }: ThumbnailsType) {
34
+ const controls = useAnimation()
35
+ const viewportSize = useWindowSize()
36
+ const thumbnailWidth = viewportSize.width / 8
37
+ const draggable = thumbnailWidth * urls.length > viewportSize.width
38
+ const css = classnames(styles.Thumbnails, {
39
+ [styles.draggable]: draggable,
40
+ })
41
+ const dragConstraints = {
42
+ left: -1 * (thumbnailWidth * urls.length - viewportSize.width),
43
+ right: 0,
44
+ }
45
+
46
+ const modifyTarget = (target) => {
47
+ const nextIndex = Math.round(Math.abs(target) / thumbnailWidth)
48
+ const snapTargetIndex = indexWithinBounds(nextIndex, 0, urls.length)
49
+ const snapTarget = snapTargetIndex * thumbnailWidth
50
+ const direction = Math.sign(target)
51
+ return direction * snapTarget
52
+ }
53
+
54
+ useEffect(() => {
55
+ if (draggable) {
56
+ const x = Math.max(-current * thumbnailWidth, dragConstraints.left)
57
+ controls.start({ x })
58
+ }
59
+ }, [controls, current, draggable, dragConstraints.left, thumbnailWidth])
60
+
61
+ return (
62
+ <motion.div
63
+ animate={controls}
64
+ className={css}
65
+ drag={draggable && 'x'}
66
+ dragConstraints={dragConstraints}
67
+ dragElastic={0.05}
68
+ dragTransition={{ modifyTarget }}
69
+ transition={{ type: 'spring', bounce: 0 }}
70
+ >
71
+ {urls.map((url, i) => (
72
+ <Thumbnail
73
+ active={i === current}
74
+ alt={i}
75
+ key={i}
76
+ onClick={() => onChange(i)}
77
+ url={url}
78
+ />
79
+ ))}
80
+ </motion.div>
81
+ )
82
+ }
@@ -0,0 +1,59 @@
1
+ /* eslint-disable jsx-control-statements/jsx-use-if-tag */
2
+ /* @flow */
3
+
4
+ import { noop } from 'lodash'
5
+ import React, { useEffect, useState } from 'react'
6
+
7
+ import Slides from './Slides'
8
+ import Thumbnails from './Thumbnails'
9
+ import styles from './styles.scss'
10
+
11
+ type CarouselType = {
12
+ initialPhoto: string,
13
+ onClose: Function,
14
+ icon: string,
15
+ iconSize: number,
16
+ current: number,
17
+ photos: Array<string>,
18
+ onChange: (index: number) => void,
19
+ onClick: (index: number) => void,
20
+ }
21
+
22
+ export default function Carousel({
23
+ current = 0,
24
+ photos,
25
+ onClick = noop,
26
+ onChange = noop,
27
+ }: CarouselType) {
28
+ useEffect(() => {
29
+ document.body.style.overflow = 'hidden'
30
+
31
+ return () => {
32
+ document.body.style.overflow = 'initial'
33
+ }
34
+ }, [])
35
+
36
+ const [currentIndex, setCurrentIndex] = useState(current)
37
+ const handleChange = (index) => {
38
+ setCurrentIndex(index)
39
+ onChange(index)
40
+ }
41
+
42
+ return (
43
+ <div className={styles.Lightbox}>
44
+ <Slides
45
+ current={currentIndex}
46
+ onChange={handleChange}
47
+ onClick={onClick}
48
+ urls={photos.map((photo) => photo.url)}
49
+ />
50
+ {photos.length > 1 ? (
51
+ <Thumbnails
52
+ current={currentIndex}
53
+ onChange={handleChange}
54
+ urls={photos.map((photo) => photo.thumbnail)}
55
+ />
56
+ ) : null}
57
+ </div>
58
+ )
59
+ }