@telus-uds/components-base 3.3.0 → 3.5.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 (54) hide show
  1. package/CHANGELOG.md +28 -2
  2. package/lib/cjs/ActivityIndicator/Dots.js +165 -0
  3. package/lib/cjs/ActivityIndicator/Dots.native.js +221 -0
  4. package/lib/cjs/ActivityIndicator/Spinner.js +57 -50
  5. package/lib/cjs/ActivityIndicator/Spinner.native.js +90 -108
  6. package/lib/cjs/ActivityIndicator/index.js +12 -1
  7. package/lib/cjs/ActivityIndicator/shared.js +53 -6
  8. package/lib/cjs/Button/ButtonBase.js +1 -1
  9. package/lib/cjs/Button/ButtonLink.js +1 -0
  10. package/lib/cjs/Carousel/Carousel.js +18 -7
  11. package/lib/cjs/ExpandCollapse/ExpandCollapse.js +3 -1
  12. package/lib/cjs/ExpandCollapseMini/ExpandCollapseMini.js +10 -1
  13. package/lib/cjs/FileUpload/FileUpload.js +31 -2
  14. package/lib/cjs/Link/Link.js +8 -1
  15. package/lib/cjs/Link/LinkBase.js +2 -0
  16. package/lib/cjs/MultiSelectFilter/MultiSelectFilter.js +3 -2
  17. package/lib/cjs/utils/containUniqueFields.js +5 -5
  18. package/lib/cjs/utils/useUniqueId.js +2 -6
  19. package/lib/esm/ActivityIndicator/Dots.js +158 -0
  20. package/lib/esm/ActivityIndicator/Dots.native.js +212 -0
  21. package/lib/esm/ActivityIndicator/Spinner.js +58 -51
  22. package/lib/esm/ActivityIndicator/Spinner.native.js +90 -110
  23. package/lib/esm/ActivityIndicator/index.js +12 -1
  24. package/lib/esm/ActivityIndicator/shared.js +52 -5
  25. package/lib/esm/Button/ButtonBase.js +2 -2
  26. package/lib/esm/Button/ButtonLink.js +2 -1
  27. package/lib/esm/Carousel/Carousel.js +18 -7
  28. package/lib/esm/ExpandCollapse/ExpandCollapse.js +4 -2
  29. package/lib/esm/ExpandCollapseMini/ExpandCollapseMini.js +11 -2
  30. package/lib/esm/FileUpload/FileUpload.js +31 -2
  31. package/lib/esm/Link/Link.js +8 -1
  32. package/lib/esm/Link/LinkBase.js +2 -0
  33. package/lib/esm/MultiSelectFilter/MultiSelectFilter.js +3 -2
  34. package/lib/esm/utils/containUniqueFields.js +5 -5
  35. package/lib/esm/utils/useUniqueId.js +3 -7
  36. package/lib/package.json +4 -3
  37. package/package.json +4 -3
  38. package/src/ActivityIndicator/Dots.jsx +200 -0
  39. package/src/ActivityIndicator/Dots.native.jsx +213 -0
  40. package/src/ActivityIndicator/Spinner.jsx +95 -59
  41. package/src/ActivityIndicator/Spinner.native.jsx +125 -132
  42. package/src/ActivityIndicator/index.jsx +17 -2
  43. package/src/ActivityIndicator/shared.js +52 -5
  44. package/src/Button/ButtonBase.jsx +4 -2
  45. package/src/Button/ButtonLink.jsx +3 -1
  46. package/src/Carousel/Carousel.jsx +28 -7
  47. package/src/ExpandCollapse/ExpandCollapse.jsx +9 -4
  48. package/src/ExpandCollapseMini/ExpandCollapseMini.jsx +15 -3
  49. package/src/FileUpload/FileUpload.jsx +32 -2
  50. package/src/Link/Link.jsx +8 -1
  51. package/src/Link/LinkBase.jsx +2 -0
  52. package/src/MultiSelectFilter/MultiSelectFilter.jsx +2 -2
  53. package/src/utils/containUniqueFields.js +5 -5
  54. package/src/utils/useUniqueId.js +3 -8
@@ -1,11 +1,7 @@
1
- import { useState } from 'react';
2
- let id = 0;
1
+ import { useId } from 'react';
3
2
  function useUniqueId() {
4
3
  let prefix = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
5
- const [uniqueId] = useState(() => {
6
- id += 1;
7
- return `${prefix}-${id}`;
8
- });
9
- return uniqueId;
4
+ const id = useId();
5
+ return `${prefix ? `${prefix}-` : ''}${id}`;
10
6
  }
11
7
  export default useUniqueId;
package/lib/package.json CHANGED
@@ -12,7 +12,7 @@
12
12
  "@gorhom/portal": "^1.0.14",
13
13
  "@react-native-picker/picker": "^2.9.0",
14
14
  "@telus-uds/system-constants": "^3.0.0",
15
- "@telus-uds/system-theme-tokens": "^4.2.0",
15
+ "@telus-uds/system-theme-tokens": "^4.3.0",
16
16
  "airbnb-prop-types": "^2.16.0",
17
17
  "css-mediaquery": "^0.1.2",
18
18
  "expo-document-picker": "^13.0.1",
@@ -59,7 +59,8 @@
59
59
  "react": "^18.2.0",
60
60
  "react-dom": "^18.2.0",
61
61
  "react-native": "^0.74.5",
62
- "react-native-web": "^0.19.10"
62
+ "react-native-web": "^0.19.10",
63
+ "react-native-svg": "15.7.1"
63
64
  },
64
65
  "react-native": "src/index.js",
65
66
  "repository": {
@@ -83,6 +84,6 @@
83
84
  "standard-engine": {
84
85
  "skip": true
85
86
  },
86
- "version": "3.3.0",
87
+ "version": "3.5.0",
87
88
  "types": "types/index.d.ts"
88
89
  }
package/package.json CHANGED
@@ -12,7 +12,7 @@
12
12
  "@gorhom/portal": "^1.0.14",
13
13
  "@react-native-picker/picker": "^2.9.0",
14
14
  "@telus-uds/system-constants": "^3.0.0",
15
- "@telus-uds/system-theme-tokens": "^4.2.0",
15
+ "@telus-uds/system-theme-tokens": "^4.3.0",
16
16
  "airbnb-prop-types": "^2.16.0",
17
17
  "css-mediaquery": "^0.1.2",
18
18
  "expo-document-picker": "^13.0.1",
@@ -59,7 +59,8 @@
59
59
  "react": "^18.2.0",
60
60
  "react-dom": "^18.2.0",
61
61
  "react-native": "^0.74.5",
62
- "react-native-web": "^0.19.10"
62
+ "react-native-web": "^0.19.10",
63
+ "react-native-svg": "15.7.1"
63
64
  },
64
65
  "react-native": "src/index.js",
65
66
  "repository": {
@@ -83,6 +84,6 @@
83
84
  "standard-engine": {
84
85
  "skip": true
85
86
  },
86
- "version": "3.3.0",
87
+ "version": "3.5.0",
87
88
  "types": "types/index.d.ts"
88
89
  }
@@ -0,0 +1,200 @@
1
+ import React from 'react'
2
+ import {
3
+ DOTS_TOTAL_ANIMATION_DURATION,
4
+ OVERSHOOT_FACTOR,
5
+ UNDERSHOOT_FACTOR,
6
+ BOUNCY_CURVE,
7
+ DOTS_SPACING_X,
8
+ DOTS_BASE_Y_FACTOR,
9
+ DOTS_JUMP_HEIGHT_FACTOR,
10
+ DOTS_PADDING_FACTOR,
11
+ DOTS_FADEOUT_KEYTIMES,
12
+ DOTS_FADEOUT_VALUES,
13
+ DOT1_COLOR_KEYTIMES,
14
+ DOT2_COLOR_KEYTIMES,
15
+ DOT3_COLOR_KEYTIMES,
16
+ DOT1_ANIMATION_KEYTIMES,
17
+ DOT2_ANIMATION_KEYTIMES,
18
+ DOT3_ANIMATION_KEYTIMES,
19
+ propTypes
20
+ } from './shared'
21
+ import { useA11yInfo } from '../A11yInfoProvider'
22
+
23
+ const CY_PROPS = {
24
+ attributeName: 'cy',
25
+ calcMode: 'spline',
26
+ dur: `${DOTS_TOTAL_ANIMATION_DURATION}ms`,
27
+ repeatCount: 'indefinite'
28
+ }
29
+
30
+ const FILL_PROPS = {
31
+ attributeName: 'fill',
32
+ calcMode: 'linear',
33
+ dur: `${DOTS_TOTAL_ANIMATION_DURATION}ms`,
34
+ repeatCount: 'indefinite'
35
+ }
36
+
37
+ const OPACITY_PROPS = {
38
+ attributeName: 'opacity',
39
+ calcMode: 'linear',
40
+ dur: `${DOTS_TOTAL_ANIMATION_DURATION}ms`,
41
+ repeatCount: 'indefinite'
42
+ }
43
+
44
+ const Dots = React.forwardRef(
45
+ ({ size, color, indicatorBackgroundColor, label, isStatic = false }, ref) => {
46
+ const { reduceMotionEnabled } = useA11yInfo()
47
+ const reduceMotion = reduceMotionEnabled || isStatic
48
+
49
+ const spacingX = size + DOTS_SPACING_X
50
+ const baseY = size * DOTS_BASE_Y_FACTOR
51
+ const jumpHeight = size * DOTS_JUMP_HEIGHT_FACTOR
52
+ const padding = size * DOTS_PADDING_FACTOR
53
+
54
+ return (
55
+ <svg
56
+ ref={ref}
57
+ aria-valuetext={label}
58
+ role="progressbar"
59
+ aria-busy="true"
60
+ width={size * 3 + spacingX * 2 + padding * 2}
61
+ height={size * 6}
62
+ viewBox={`0 0 ${size * 3 + spacingX * 2 + padding * 2} ${size * 6}`}
63
+ >
64
+ <g>
65
+ {/* ===================== DOT 1 ===================== */}
66
+ <circle cx={padding + size / 2} cy={baseY} r={size / 2} fill={color}>
67
+ {!reduceMotion && (
68
+ <>
69
+ <animate
70
+ {...CY_PROPS}
71
+ keyTimes={DOT1_ANIMATION_KEYTIMES}
72
+ keySplines={`${BOUNCY_CURVE};${BOUNCY_CURVE};${BOUNCY_CURVE}`}
73
+ values={`
74
+ ${baseY};
75
+ ${baseY - jumpHeight * OVERSHOOT_FACTOR};
76
+ ${baseY + jumpHeight * UNDERSHOOT_FACTOR};
77
+ ${baseY}
78
+ `}
79
+ />
80
+ {/* color (fill) */}
81
+ <animate
82
+ {...FILL_PROPS}
83
+ keyTimes={DOT1_COLOR_KEYTIMES}
84
+ values={`
85
+ ${color};
86
+ ${color};
87
+ ${indicatorBackgroundColor};
88
+ ${indicatorBackgroundColor}
89
+ `}
90
+ />
91
+ {/* fade-out */}
92
+ <animate
93
+ {...OPACITY_PROPS}
94
+ keyTimes={DOTS_FADEOUT_KEYTIMES}
95
+ values={DOTS_FADEOUT_VALUES}
96
+ />
97
+ </>
98
+ )}
99
+ </circle>
100
+
101
+ {/* ===================== DOT 2 ===================== */}
102
+ <circle
103
+ cx={padding + size + spacingX + size / 2}
104
+ cy={baseY}
105
+ r={size / 2}
106
+ fill={indicatorBackgroundColor}
107
+ >
108
+ {reduceMotion ? null : (
109
+ <>
110
+ <animate
111
+ {...CY_PROPS}
112
+ keyTimes={DOT2_ANIMATION_KEYTIMES}
113
+ keySplines={`
114
+ ${BOUNCY_CURVE};
115
+ ${BOUNCY_CURVE};
116
+ ${BOUNCY_CURVE};
117
+ ${BOUNCY_CURVE}
118
+ `}
119
+ values={`
120
+ ${baseY};
121
+ ${baseY};
122
+ ${baseY - jumpHeight * OVERSHOOT_FACTOR};
123
+ ${baseY + jumpHeight * UNDERSHOOT_FACTOR};
124
+ ${baseY}
125
+ `}
126
+ />
127
+ {/* color (fill) */}
128
+ <animate
129
+ {...FILL_PROPS}
130
+ keyTimes={DOT2_COLOR_KEYTIMES}
131
+ values={`
132
+ ${indicatorBackgroundColor};
133
+ ${indicatorBackgroundColor};
134
+ ${color};
135
+ ${color};
136
+ ${indicatorBackgroundColor};
137
+ ${indicatorBackgroundColor}
138
+ `}
139
+ />
140
+ {/* fade-out */}
141
+ <animate
142
+ {...OPACITY_PROPS}
143
+ keyTimes={DOTS_FADEOUT_KEYTIMES}
144
+ values={DOTS_FADEOUT_VALUES}
145
+ />
146
+ </>
147
+ )}
148
+ </circle>
149
+
150
+ {/* ===================== DOT 3 ===================== */}
151
+ <circle
152
+ cx={padding + (size + spacingX) * 2 + size / 2}
153
+ cy={baseY}
154
+ r={size / 2}
155
+ fill={indicatorBackgroundColor}
156
+ >
157
+ {reduceMotion ? null : (
158
+ <>
159
+ <animate
160
+ {...CY_PROPS}
161
+ keyTimes={DOT3_ANIMATION_KEYTIMES}
162
+ keySplines={`
163
+ ${BOUNCY_CURVE};
164
+ ${BOUNCY_CURVE};
165
+ ${BOUNCY_CURVE};
166
+ ${BOUNCY_CURVE}
167
+ `}
168
+ values={`
169
+ ${baseY};
170
+ ${baseY};
171
+ ${baseY - jumpHeight * OVERSHOOT_FACTOR};
172
+ ${baseY + jumpHeight * UNDERSHOOT_FACTOR};
173
+ ${baseY}
174
+ `}
175
+ />
176
+ {/* color (fill) */}
177
+ <animate
178
+ {...FILL_PROPS}
179
+ keyTimes={DOT3_COLOR_KEYTIMES}
180
+ values={`
181
+ ${indicatorBackgroundColor};
182
+ ${indicatorBackgroundColor};
183
+ ${color};
184
+ ${color};
185
+ ${color}
186
+ `}
187
+ />
188
+ </>
189
+ )}
190
+ </circle>
191
+ </g>
192
+ </svg>
193
+ )
194
+ }
195
+ )
196
+
197
+ Dots.displayName = 'Dots'
198
+ Dots.propTypes = propTypes
199
+
200
+ export default Dots
@@ -0,0 +1,213 @@
1
+ import React from 'react'
2
+ import { View, StyleSheet, Animated, Easing } from 'react-native'
3
+ import Svg, { Circle } from 'react-native-svg'
4
+ import {
5
+ DOTS_TOTAL_ANIMATION_DURATION,
6
+ OVERSHOOT_FACTOR,
7
+ UNDERSHOOT_FACTOR,
8
+ DOTS_SPACING_X,
9
+ DOTS_BASE_Y_FACTOR,
10
+ DOTS_JUMP_HEIGHT_FACTOR,
11
+ DOTS_PADDING_FACTOR,
12
+ DOT_FADEOUT_INPUT_RANGE,
13
+ DOT_FADEOUT_OUTPUT_RANGE,
14
+ DOT1_ANIMATION_INPUT_RANGE,
15
+ DOT2_ANIMATION_INPUT_RANGE,
16
+ DOT3_ANIMATION_INPUT_RANGE,
17
+ propTypes
18
+ } from './shared'
19
+ import { useA11yInfo } from '../A11yInfoProvider'
20
+
21
+ const AnimatedCircle = Animated.createAnimatedComponent(Circle)
22
+
23
+ const Dots = React.forwardRef(
24
+ ({ size, color, indicatorBackgroundColor, label, isStatic = false }, ref) => {
25
+ const { reduceMotionEnabled } = useA11yInfo()
26
+ const reduceMotion = reduceMotionEnabled || isStatic
27
+
28
+ const spacingX = size + DOTS_SPACING_X
29
+ const baseY = size * DOTS_BASE_Y_FACTOR
30
+ const jumpHeight = size * DOTS_JUMP_HEIGHT_FACTOR
31
+ const padding = size * DOTS_PADDING_FACTOR
32
+ const width = (size + 1) * 6
33
+ const height = size * 6
34
+
35
+ const animationValue = React.useRef(new Animated.Value(0)).current
36
+
37
+ React.useEffect(() => {
38
+ if (reduceMotion) {
39
+ animationValue.stopAnimation()
40
+ animationValue.setValue(0)
41
+ return () => {}
42
+ }
43
+ const loopAnimation = Animated.loop(
44
+ Animated.timing(animationValue, {
45
+ toValue: 1,
46
+ duration: DOTS_TOTAL_ANIMATION_DURATION,
47
+ easing: Easing.linear,
48
+ useNativeDriver: false
49
+ })
50
+ )
51
+ loopAnimation.start()
52
+ return () => loopAnimation.stop()
53
+ }, [reduceMotion, animationValue])
54
+
55
+ const makeStepInterpolation = (steps) => {
56
+ const inputRange = []
57
+ const outputRange = []
58
+ steps.forEach(({ time, value }) => {
59
+ inputRange.push(time)
60
+ outputRange.push(value)
61
+ })
62
+ return { inputRange, outputRange }
63
+ }
64
+
65
+ // ---- DOT 1 ----
66
+ const dot1Cy = animationValue.interpolate({
67
+ inputRange: DOT1_ANIMATION_INPUT_RANGE,
68
+ outputRange: [
69
+ baseY,
70
+ baseY - jumpHeight * OVERSHOOT_FACTOR,
71
+ baseY + jumpHeight * UNDERSHOOT_FACTOR,
72
+ baseY
73
+ ],
74
+ extrapolate: 'clamp'
75
+ })
76
+
77
+ const { inputRange: dot1FillInputRange, outputRange: dot1FillOutputRange } =
78
+ makeStepInterpolation([
79
+ { time: 0, value: color },
80
+ { time: 0.3, value: color },
81
+ { time: 0.4, value: indicatorBackgroundColor },
82
+ { time: 1, value: indicatorBackgroundColor }
83
+ ])
84
+ const dot1Fill = animationValue.interpolate({
85
+ inputRange: dot1FillInputRange,
86
+ outputRange: dot1FillOutputRange,
87
+ extrapolate: 'clamp'
88
+ })
89
+
90
+ // Fade-out Dot 1
91
+ const dot1Opacity = animationValue.interpolate({
92
+ inputRange: DOT_FADEOUT_INPUT_RANGE,
93
+ outputRange: DOT_FADEOUT_OUTPUT_RANGE,
94
+ extrapolate: 'clamp'
95
+ })
96
+
97
+ // ---- DOT 2 ----
98
+ const dot2Cy = animationValue.interpolate({
99
+ inputRange: DOT2_ANIMATION_INPUT_RANGE,
100
+ outputRange: [
101
+ baseY,
102
+ baseY,
103
+ baseY - jumpHeight * OVERSHOOT_FACTOR,
104
+ baseY + jumpHeight * UNDERSHOOT_FACTOR,
105
+ baseY
106
+ ],
107
+ extrapolate: 'clamp'
108
+ })
109
+
110
+ const { inputRange: dot2FillInputRange, outputRange: dot2FillOutputRange } =
111
+ makeStepInterpolation([
112
+ { time: 0, value: indicatorBackgroundColor },
113
+ { time: 0.3, value: indicatorBackgroundColor },
114
+ { time: 0.4, value: color },
115
+ { time: 0.6, value: color },
116
+ { time: 0.7, value: indicatorBackgroundColor },
117
+ { time: 1, value: indicatorBackgroundColor }
118
+ ])
119
+ const dot2Fill = animationValue.interpolate({
120
+ inputRange: dot2FillInputRange,
121
+ outputRange: dot2FillOutputRange,
122
+ extrapolate: 'clamp'
123
+ })
124
+
125
+ // Fade-out Dot 2
126
+ const dot2Opacity = animationValue.interpolate({
127
+ inputRange: DOT_FADEOUT_INPUT_RANGE,
128
+ outputRange: DOT_FADEOUT_OUTPUT_RANGE,
129
+ extrapolate: 'clamp'
130
+ })
131
+
132
+ // ---- DOT 3 ----
133
+ const dot3Cy = animationValue.interpolate({
134
+ inputRange: DOT3_ANIMATION_INPUT_RANGE,
135
+ outputRange: [
136
+ baseY,
137
+ baseY,
138
+ baseY - jumpHeight * OVERSHOOT_FACTOR,
139
+ baseY + jumpHeight * UNDERSHOOT_FACTOR,
140
+ baseY
141
+ ],
142
+ extrapolate: 'clamp'
143
+ })
144
+
145
+ const { inputRange: dot3FillInputRange, outputRange: dot3FillOutputRange } =
146
+ makeStepInterpolation([
147
+ { time: 0, value: indicatorBackgroundColor },
148
+ { time: 0.5, value: indicatorBackgroundColor },
149
+ { time: 0.6, value: color },
150
+ { time: 0.8, value: color },
151
+ { time: 1, value: color }
152
+ ])
153
+ const dot3Fill = animationValue.interpolate({
154
+ inputRange: dot3FillInputRange,
155
+ outputRange: dot3FillOutputRange,
156
+ extrapolate: 'clamp'
157
+ })
158
+
159
+ const dot3Opacity = new Animated.Value(1)
160
+
161
+ return (
162
+ <View
163
+ ref={ref}
164
+ style={styles.container}
165
+ accessible
166
+ accessibilityLabel={label}
167
+ accessibilityRole="progressbar"
168
+ accessibilityState={{ busy: true }}
169
+ >
170
+ <Svg width={width} height={height} viewBox={`0 0 ${width} ${height}`}>
171
+ {/* DOT 1 */}
172
+ <AnimatedCircle
173
+ cx={size}
174
+ cy={reduceMotion ? baseY : dot1Cy}
175
+ r={padding}
176
+ fill={reduceMotion ? color : dot1Fill}
177
+ opacity={reduceMotion ? 1 : dot1Opacity}
178
+ />
179
+
180
+ {/* DOT 2 */}
181
+ <AnimatedCircle
182
+ cx={2 * size + spacingX}
183
+ cy={reduceMotion ? baseY : dot2Cy}
184
+ r={padding}
185
+ fill={reduceMotion ? indicatorBackgroundColor : dot2Fill}
186
+ opacity={reduceMotion ? 1 : dot2Opacity}
187
+ />
188
+
189
+ {/* DOT 3 */}
190
+ <AnimatedCircle
191
+ cx={3 * size + 2 * spacingX}
192
+ cy={reduceMotion ? baseY : dot3Cy}
193
+ r={padding}
194
+ fill={reduceMotion ? indicatorBackgroundColor : dot3Fill}
195
+ opacity={reduceMotion ? 1 : dot3Opacity}
196
+ />
197
+ </Svg>
198
+ </View>
199
+ )
200
+ }
201
+ )
202
+
203
+ Dots.displayName = 'Dots'
204
+ Dots.propTypes = propTypes
205
+
206
+ const styles = StyleSheet.create({
207
+ container: {
208
+ alignItems: 'center',
209
+ justifyContent: 'center'
210
+ }
211
+ })
212
+
213
+ export default Dots
@@ -1,83 +1,119 @@
1
1
  import React from 'react'
2
- import { DURATION, MIN_EMPTY_ANGLE, MIN_STROKE_ANGLE, BEZIER, propTypes } from './shared'
2
+ import {
3
+ DURATION,
4
+ SVG_CIRCUMFERENCE,
5
+ SVG_SIZE,
6
+ ROTATION_TRANSFORM,
7
+ SVG_CENTER,
8
+ SPINNER_RADIUS,
9
+ SPINNER_ROTATION_DEGREES,
10
+ SPINNER_DASHARRAY_MIN,
11
+ SPINNER_DASHARRAY_MAX,
12
+ SPINNER_KEYTIMES,
13
+ SPINNER_DASHARRAY_HALF,
14
+ SPINNER_DASHOFFSET_FACTOR,
15
+ propTypes
16
+ } from './shared'
3
17
  import { useA11yInfo } from '../A11yInfoProvider'
4
18
 
5
- const SVG_RADIUS = 20
6
- const SVG_CIRCUMFERENCE = SVG_RADIUS * 2 * Math.PI
7
- const MIN_SVG_LENGTH = (MIN_STROKE_ANGLE / 360) * SVG_CIRCUMFERENCE
8
- const MAX_SVG_LENGTH = (1 - MIN_EMPTY_ANGLE / 360) * SVG_CIRCUMFERENCE
9
-
10
- const animationProps = {
11
- begin: '0s',
12
- dur: `${DURATION}ms`,
13
- fill: 'freeze',
19
+ const animateProps = {
20
+ keyTimes: SPINNER_KEYTIMES,
14
21
  repeatCount: 'indefinite'
15
22
  }
16
23
 
17
- const bezierProps = {
18
- calcMode: 'spline',
19
- keyTimes: '0; 0.5; 1',
20
- keySplines: `${BEZIER.join(', ')} ; ${BEZIER.join(', ')}`
21
- }
22
- // We're using svg rather than css here to define the animation to avoid needing to introduce css injection mechanism
23
- // It's possible to replicate this functionality with RNW animations, but it snags on chrome at least, see https://github.com/telus/universal-design-system/pull/477 for details.
24
- const Spinner = React.forwardRef(({ size, color, thickness, label, isStatic = false }, ref) => {
25
- const { reduceMotionEnabled } = useA11yInfo()
26
- const reduceMotion = reduceMotionEnabled || isStatic
27
- return (
28
- <svg
29
- ref={ref}
30
- width={`${size}px`}
31
- height={`${size}px`}
32
- viewBox="0 0 48 48"
33
- aria-valuetext={label}
34
- role="progressbar"
35
- aria-busy={true}
36
- >
37
- <g>
38
- {reduceMotion ? null : (
39
- <animateTransform
40
- attributeName="transform"
41
- type="rotate"
42
- values={`-180 24 24;${360 + MIN_STROKE_ANGLE - 180} 24 24`}
43
- {...animationProps}
44
- />
45
- )}
24
+ const Spinner = React.forwardRef(
25
+ ({ size, color, indicatorBackgroundColor, thickness, label, isStatic = false }, ref) => {
26
+ const { reduceMotionEnabled } = useA11yInfo()
27
+ const reduceMotion = reduceMotionEnabled || isStatic
28
+
29
+ const strokeWidth = (thickness * SVG_SIZE) / size
30
+
31
+ return (
32
+ <svg
33
+ ref={ref}
34
+ width={`${size}px`}
35
+ height={`${size}px`}
36
+ viewBox={`0 0 ${SVG_SIZE} ${SVG_SIZE}`}
37
+ aria-valuetext={label}
38
+ role="progressbar"
39
+ aria-busy="true"
40
+ >
41
+ {/* Base static circle with background color */}
42
+ <circle
43
+ fill="none"
44
+ stroke={indicatorBackgroundColor}
45
+ strokeWidth={strokeWidth}
46
+ cx={SVG_CENTER}
47
+ cy={SVG_CENTER}
48
+ r={SPINNER_RADIUS}
49
+ />
50
+
51
+ {/* Animated circle */}
46
52
  <circle
47
53
  fill="none"
48
54
  stroke={color}
49
- strokeWidth={(thickness * 48) / size}
55
+ strokeWidth={strokeWidth}
50
56
  strokeLinecap="round"
51
- cx="24"
52
- cy="24"
53
- r="20"
54
- strokeDasharray={reduceMotion ? MAX_SVG_LENGTH : [MIN_SVG_LENGTH, SVG_CIRCUMFERENCE]}
55
- strokeDashoffset={0}
56
- vectorEffect="non-scaling-stroke"
57
+ cx={SVG_CENTER}
58
+ cy={SVG_CENTER}
59
+ r={SPINNER_RADIUS}
60
+ strokeDasharray={`${SPINNER_DASHARRAY_MIN * SVG_CIRCUMFERENCE}, ${
61
+ SPINNER_DASHARRAY_MAX * SVG_CIRCUMFERENCE
62
+ }`}
63
+ strokeDashoffset="0"
64
+ // Circle rotated -90° so that 0% is at the top (12 o'clock).
65
+ transform={ROTATION_TRANSFORM}
57
66
  >
58
67
  {reduceMotion ? null : (
59
68
  <>
60
- <animate
61
- attributeName="stroke-dashoffset"
62
- values={`0;-10;${MIN_SVG_LENGTH - SVG_CIRCUMFERENCE}`}
63
- {...animationProps}
64
- {...bezierProps}
69
+ {/*
70
+ Rotation animated from 0° to 183°,
71
+ with additive="sum" is added to the base rotation of -90°.
72
+ */}
73
+ <animateTransform
74
+ attributeName="transform"
75
+ type="rotate"
76
+ from="0 24 24"
77
+ to={`${SPINNER_ROTATION_DEGREES} ${SVG_CENTER} ${SVG_CENTER}`}
78
+ dur={`${DURATION}ms`}
79
+ repeatCount="indefinite"
80
+ additive="sum"
65
81
  />
66
82
  <animate
67
83
  attributeName="stroke-dasharray"
68
- values={`${MIN_SVG_LENGTH}, 200;${MAX_SVG_LENGTH}, 200;${MIN_SVG_LENGTH}, 200`}
69
- {...animationProps}
70
- {...bezierProps}
84
+ dur={`${DURATION}ms`}
85
+ values={`
86
+ ${SPINNER_DASHARRAY_MIN * SVG_CIRCUMFERENCE},${
87
+ SPINNER_DASHARRAY_MAX * SVG_CIRCUMFERENCE
88
+ };
89
+ ${SPINNER_DASHARRAY_HALF * SVG_CIRCUMFERENCE},${
90
+ SPINNER_DASHARRAY_HALF * SVG_CIRCUMFERENCE
91
+ };
92
+ ${SPINNER_DASHARRAY_MIN * SVG_CIRCUMFERENCE},${
93
+ SPINNER_DASHARRAY_MAX * SVG_CIRCUMFERENCE
94
+ }
95
+ `}
96
+ {...animateProps}
97
+ />
98
+ <animate
99
+ attributeName="stroke-dashoffset"
100
+ dur={`${DURATION}ms`}
101
+ values={`
102
+ 0;
103
+ 0;
104
+ -${SPINNER_DASHOFFSET_FACTOR * SVG_CIRCUMFERENCE}
105
+ `}
106
+ {...animateProps}
71
107
  />
72
108
  </>
73
109
  )}
74
110
  </circle>
75
- </g>
76
- </svg>
77
- )
78
- })
79
- Spinner.displayName = 'Spinner'
111
+ </svg>
112
+ )
113
+ }
114
+ )
80
115
 
116
+ Spinner.displayName = 'Spinner'
81
117
  Spinner.propTypes = propTypes
82
118
 
83
119
  export default Spinner