@telus-uds/components-base 3.7.0 → 3.8.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 (51) hide show
  1. package/CHANGELOG.md +27 -2
  2. package/lib/cjs/ActivityIndicator/FullScreenIndicator.js +89 -0
  3. package/lib/cjs/ActivityIndicator/InlineIndicator.js +64 -0
  4. package/lib/cjs/ActivityIndicator/OverlayIndicator.js +156 -0
  5. package/lib/cjs/ActivityIndicator/RenderActivityIndicator.js +88 -0
  6. package/lib/cjs/ActivityIndicator/index.js +91 -23
  7. package/lib/cjs/ActivityIndicator/shared.js +12 -1
  8. package/lib/cjs/ActivityIndicator/sharedProptypes.js +67 -0
  9. package/lib/cjs/Card/Card.js +38 -45
  10. package/lib/cjs/Card/PressableCardBase.js +4 -1
  11. package/lib/cjs/ExpandCollapseMini/ExpandCollapseMiniControl.js +1 -1
  12. package/lib/cjs/List/ListItemMark.js +13 -2
  13. package/lib/cjs/MultiSelectFilter/ModalOverlay.js +12 -3
  14. package/lib/cjs/MultiSelectFilter/MultiSelectFilter.js +9 -2
  15. package/lib/cjs/utils/index.js +9 -1
  16. package/lib/cjs/utils/useDetectOutsideClick.js +39 -0
  17. package/lib/cjs/utils/useVariants.js +46 -0
  18. package/lib/esm/ActivityIndicator/FullScreenIndicator.js +82 -0
  19. package/lib/esm/ActivityIndicator/InlineIndicator.js +57 -0
  20. package/lib/esm/ActivityIndicator/OverlayIndicator.js +149 -0
  21. package/lib/esm/ActivityIndicator/RenderActivityIndicator.js +83 -0
  22. package/lib/esm/ActivityIndicator/index.js +89 -23
  23. package/lib/esm/ActivityIndicator/shared.js +11 -0
  24. package/lib/esm/ActivityIndicator/sharedProptypes.js +61 -0
  25. package/lib/esm/Card/Card.js +38 -45
  26. package/lib/esm/Card/PressableCardBase.js +4 -1
  27. package/lib/esm/ExpandCollapseMini/ExpandCollapseMiniControl.js +1 -1
  28. package/lib/esm/List/ListItemMark.js +13 -2
  29. package/lib/esm/MultiSelectFilter/ModalOverlay.js +12 -3
  30. package/lib/esm/MultiSelectFilter/MultiSelectFilter.js +9 -2
  31. package/lib/esm/utils/index.js +2 -1
  32. package/lib/esm/utils/useDetectOutsideClick.js +31 -0
  33. package/lib/esm/utils/useVariants.js +41 -0
  34. package/lib/package.json +2 -2
  35. package/package.json +2 -2
  36. package/src/ActivityIndicator/FullScreenIndicator.jsx +65 -0
  37. package/src/ActivityIndicator/InlineIndicator.jsx +47 -0
  38. package/src/ActivityIndicator/OverlayIndicator.jsx +140 -0
  39. package/src/ActivityIndicator/RenderActivityIndicator.jsx +82 -0
  40. package/src/ActivityIndicator/index.jsx +113 -32
  41. package/src/ActivityIndicator/shared.js +11 -0
  42. package/src/ActivityIndicator/sharedProptypes.js +62 -0
  43. package/src/Card/Card.jsx +51 -54
  44. package/src/Card/PressableCardBase.jsx +1 -1
  45. package/src/ExpandCollapseMini/ExpandCollapseMiniControl.jsx +1 -1
  46. package/src/List/ListItemMark.jsx +18 -2
  47. package/src/MultiSelectFilter/ModalOverlay.jsx +15 -3
  48. package/src/MultiSelectFilter/MultiSelectFilter.jsx +9 -2
  49. package/src/utils/index.js +1 -0
  50. package/src/utils/useDetectOutsideClick.js +35 -0
  51. package/src/utils/useVariants.js +44 -0
package/src/Card/Card.jsx CHANGED
@@ -128,6 +128,7 @@ const Card = React.forwardRef(
128
128
  const selected = interactiveCard?.variant?.selected
129
129
  const inactive = interactiveCard?.variant?.inactive
130
130
  const selectionType = interactiveCard?.selectionType
131
+ const isControl = interactiveCard?.variant?.isControl === true
131
132
 
132
133
  const getThemeTokens = useThemeTokensCallback('Card', interactiveCard?.tokens, {
133
134
  interactive: true,
@@ -196,6 +197,10 @@ const Card = React.forwardRef(
196
197
  }
197
198
 
198
199
  const renderInputPerSelectionType = (props) => {
200
+ if (!isControl) {
201
+ return null
202
+ }
203
+
199
204
  switch (selectionType) {
200
205
  case SelectionType.Checkbox:
201
206
  return (
@@ -214,10 +219,6 @@ const Card = React.forwardRef(
214
219
  }
215
220
  }
216
221
 
217
- const renderNoSelectionView = () => (
218
- <View style={{ paddingTop, paddingBottom, paddingLeft, paddingRight }}>{children}</View>
219
- )
220
-
221
222
  return (
222
223
  <>
223
224
  <CardBase
@@ -227,57 +228,53 @@ const Card = React.forwardRef(
227
228
  dataSet={mediaIds && { media: mediaIds }}
228
229
  {...selectProps(rest)}
229
230
  >
230
- {interactiveCard?.body ? (
231
- <>
232
- <PressableCardBase
233
- ref={ref}
234
- tokens={getThemeTokens}
235
- dataSet={dataSet}
236
- onPress={onPress}
237
- href={interactiveCard?.href}
238
- hrefAttrs={interactiveCard?.hrefAttrs}
239
- {...selectProps(rest)}
240
- >
241
- {(cardState) => {
242
- const {
243
- iconColor: checkColor,
244
- inputBackgroundColor: boxBackgroundColor,
245
- iconBackgroundColor: checkBackgroundColor
246
- } = getThemeTokens(
247
- {
248
- ...cardState,
249
- selected,
250
- interactive: true,
251
- isControl: true
252
- },
253
- interactiveCard?.tokens
254
- )
255
- return (
256
- <>
257
- {renderInputPerSelectionType(
258
- getInputProps({
259
- id,
260
- checkColor,
261
- boxBackgroundColor,
262
- checkBackgroundColor,
263
- isControlled: true,
264
- isChecked: selected || cardState?.hover,
265
- isInactive: inactive,
266
- onPress
267
- })
268
- )}
269
- {typeof interactiveCard?.body === 'function'
270
- ? interactiveCard.body(cardState)
271
- : interactiveCard.body}
272
- </>
273
- )
274
- }}
275
- </PressableCardBase>
276
- {children && selectionType !== SelectionType.None ? renderNoSelectionView() : null}
277
- </>
278
- ) : (
279
- children
231
+ {interactiveCard?.body && (
232
+ <PressableCardBase
233
+ ref={ref}
234
+ tokens={getThemeTokens}
235
+ dataSet={dataSet}
236
+ onPress={onPress}
237
+ href={interactiveCard?.href}
238
+ hrefAttrs={interactiveCard?.hrefAttrs}
239
+ {...selectProps(rest)}
240
+ >
241
+ {(cardState) => {
242
+ const {
243
+ iconColor: checkColor,
244
+ inputBackgroundColor: boxBackgroundColor,
245
+ iconBackgroundColor: checkBackgroundColor
246
+ } = getThemeTokens(
247
+ {
248
+ ...cardState,
249
+ selected,
250
+ interactive: true,
251
+ isControl
252
+ },
253
+ interactiveCard?.tokens
254
+ )
255
+ return (
256
+ <>
257
+ {renderInputPerSelectionType(
258
+ getInputProps({
259
+ id,
260
+ checkColor,
261
+ boxBackgroundColor,
262
+ checkBackgroundColor,
263
+ isControlled: true,
264
+ isChecked: selected || cardState?.hover,
265
+ isInactive: inactive,
266
+ onPress
267
+ })
268
+ )}
269
+ {typeof interactiveCard?.body === 'function'
270
+ ? interactiveCard.body(cardState)
271
+ : interactiveCard.body}
272
+ </>
273
+ )
274
+ }}
275
+ </PressableCardBase>
280
276
  )}
277
+ {children}
281
278
  </CardBase>
282
279
  </>
283
280
  )
@@ -158,7 +158,7 @@ const PressableCardBase = React.forwardRef(
158
158
  setFocused(false)
159
159
  setPressed(false)
160
160
  }}
161
- style={staticStyles.container}
161
+ style={{ ...staticStyles.container, textDecoration: 'none' }}
162
162
  {...(hrefAttrs || {})}
163
163
  >
164
164
  <CardBase tokens={getCardTokens({ pressed, focused, hovered })}>
@@ -35,7 +35,7 @@ const ExpandCollapseMiniControl = React.forwardRef(
35
35
  const linkTokens = useThemeTokens(
36
36
  'Link',
37
37
  {},
38
- { ...variant, quiet: expanded ?? quiet },
38
+ { ...variant, quiet },
39
39
  {
40
40
  focus: isFocusVisible,
41
41
  hover,
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types'
3
3
 
4
4
  import { View, StyleSheet } from 'react-native'
5
5
  import Icon from '../Icon'
6
+ import { useVariants } from '../utils'
6
7
 
7
8
  export const tokenTypes = {
8
9
  itemIconSize: PropTypes.number.isRequired,
@@ -37,6 +38,9 @@ const selectBulletContainerStyles = ({
37
38
  alignItems: itemBulletContainerAlign
38
39
  })
39
40
 
41
+ const getIconColorVariants = (iconVariants) =>
42
+ iconVariants?.filter((variant) => variant[0] === 'color').map((variant) => variant[1])
43
+
40
44
  /**
41
45
  * Subcomponent used within ListItem and similar components for rendering bullets or icons
42
46
  * that sit alongside a ListIconContent in a { flexDirection: row } container.
@@ -50,14 +54,26 @@ const ListItemMark = React.forwardRef(({ icon, iconColor, iconSize, tokens = {}
50
54
  const sideItemContainerStyles = selectSideItemContainerStyles(themeTokens)
51
55
  const bulletContainerStyles = selectBulletContainerStyles(themeTokens)
52
56
 
57
+ // TODO: Remove it when iconColor custom colors are deprecated.
58
+ const iconVariants = useVariants('Icon')
59
+ const iconColorVariants = getIconColorVariants(iconVariants)
60
+
53
61
  if (icon) {
54
62
  const iconTokens = selectItemIconTokens(themeTokens)
55
63
  return (
56
64
  <View style={[sideItemContainerStyles, bulletContainerStyles]}>
57
65
  <Icon
58
66
  icon={icon}
59
- tokens={{ size: iconSize ?? iconTokens.size }}
60
- variant={{ color: iconColor ?? iconTokens.color }}
67
+ tokens={{
68
+ size: iconSize ?? iconTokens.size,
69
+ ...(((iconColor && !iconColorVariants?.includes(iconColor)) || !iconColor) && {
70
+ color:
71
+ iconColor && !iconColorVariants?.includes(iconColor) ? iconColor : iconTokens.color
72
+ })
73
+ }}
74
+ variant={{
75
+ ...(iconColorVariants?.includes(iconColor) && { color: iconColor })
76
+ }}
61
77
  />
62
78
  </View>
63
79
  )
@@ -9,6 +9,7 @@ import dictionary from './dictionary'
9
9
 
10
10
  import Card from '../Card'
11
11
  import IconButton from '../IconButton'
12
+ import useDetectOutsideClick from '../utils/useDetectOutsideClick'
12
13
 
13
14
  const staticStyles = StyleSheet.create({
14
15
  positioner: {
@@ -80,12 +81,17 @@ const ModalOverlay = React.forwardRef(
80
81
  tokens,
81
82
  copy,
82
83
  onClose,
83
- enableFullscreen = false
84
+ enableFullscreen = false,
85
+ dismissWhenPressedOutside = false
84
86
  },
85
87
  ref
86
88
  ) => {
87
89
  const viewport = useViewport()
88
90
  const themeTokens = useThemeTokens('Modal', tokens, variant, { viewport, maxWidth: false })
91
+
92
+ const containerRef = React.useRef(ref || null)
93
+ useDetectOutsideClick(containerRef, onClose, dismissWhenPressedOutside)
94
+
89
95
  const containerWidthHeight = {
90
96
  minWidth: tokens.maxWidth ? maxWidthSize : minWidth,
91
97
  minHeight: maxHeight ? maxHeightSize : minHeight,
@@ -101,7 +107,7 @@ const ModalOverlay = React.forwardRef(
101
107
  return (
102
108
  <Portal>
103
109
  <View
104
- ref={ref}
110
+ ref={containerRef}
105
111
  onLayout={onLayout}
106
112
  style={[
107
113
  overlaidPosition,
@@ -132,6 +138,7 @@ const ModalOverlay = React.forwardRef(
132
138
  )
133
139
  }
134
140
  )
141
+
135
142
  ModalOverlay.displayName = 'ModalOverlay'
136
143
 
137
144
  ModalOverlay.propTypes = {
@@ -153,7 +160,12 @@ ModalOverlay.propTypes = {
153
160
  tokens: getTokensPropType('Modal'),
154
161
  copy: copyPropTypes,
155
162
  onClose: PropTypes.func,
156
- enableFullscreen: PropTypes.bool
163
+ enableFullscreen: PropTypes.bool,
164
+ /**
165
+ * If true, clicking outside the content will trigger the a close callback, dismissing the content.
166
+ * @deprecated This parameter will be removed in the next major release; detection will be always enabled by default.
167
+ */
168
+ dismissWhenPressedOutside: PropTypes.bool
157
169
  }
158
170
 
159
171
  export default ModalOverlay
@@ -89,6 +89,7 @@ const MultiSelectFilter = React.forwardRef(
89
89
  inactive = false,
90
90
  rowLimit = 12,
91
91
  dictionary = defaultDictionary,
92
+ dismissWhenPressedOutside = false,
92
93
  ...rest
93
94
  },
94
95
  ref
@@ -386,8 +387,9 @@ const MultiSelectFilter = React.forwardRef(
386
387
  )}
387
388
  {isOpen && viewport !== 'xs' && (
388
389
  <ModalOverlay
389
- overlaidPosition={overlaidPosition}
390
+ dismissWhenPressedOutside={dismissWhenPressedOutside}
390
391
  onClose={onClose}
392
+ overlaidPosition={overlaidPosition}
391
393
  maxHeight={items.length > MAX_ITEMS_THRESHOLD ? true : maxHeight}
392
394
  maxHeightSize={maxHeightSize}
393
395
  maxWidthSize={maxWidthSize}
@@ -539,7 +541,12 @@ MultiSelectFilter.propTypes = {
539
541
  * Sets the maximum number of items in one column. If number of items are more
540
542
  * than the `rowLimit`, they will be rendered in 2 columns.
541
543
  */
542
- rowLimit: PropTypes.number
544
+ rowLimit: PropTypes.number,
545
+ /**
546
+ * If true, clicking outside the content will trigger the a close callback, dismissing the content.
547
+ * @deprecated This parameter will be removed in the next major release; detection will be always enabled by default.
548
+ */
549
+ dismissWhenPressedOutside: PropTypes.bool
543
550
  }
544
551
 
545
552
  export default MultiSelectFilter
@@ -24,3 +24,4 @@ export { transformGradient } from './transformGradient'
24
24
  export { default as convertFromMegaByteToByte } from './convertFromMegaByteToByte'
25
25
  export { default as formatImageSource } from './formatImageSource'
26
26
  export { default as getSpacingScale } from './getSpacingScale'
27
+ export { default as useVariants } from './useVariants'
@@ -0,0 +1,35 @@
1
+ import React from 'react'
2
+ import { Platform } from 'react-native'
3
+
4
+ /**
5
+ * Hook to detect clicks outside of a ref, only on web.
6
+ *
7
+ * @param {React.RefObject<HTMLElement>} ref
8
+ * Reference to the element you want to “protect.”
9
+ * @param {() => void} onOutside
10
+ * Callback invoked when a click occurs outside that ref.
11
+ * @param {boolean} [enabled=true]
12
+ * Flag to enable or disable the outside-click detection at runtime.
13
+ * @deprecated Will be removed in next major release; detection will always be enabled.
14
+ */
15
+
16
+ function useDetectOutsideClick(ref, onOutside, enabled = true) {
17
+ React.useEffect(() => {
18
+ if (!enabled || Platform.OS !== 'web') {
19
+ return undefined
20
+ }
21
+
22
+ const handleClickOutside = (e) => {
23
+ if (ref.current && !ref.current.contains(e.target)) {
24
+ onOutside()
25
+ }
26
+ }
27
+
28
+ document.addEventListener('mousedown', handleClickOutside)
29
+ return () => {
30
+ document.removeEventListener('mousedown', handleClickOutside)
31
+ }
32
+ }, [ref, onOutside, enabled])
33
+ }
34
+
35
+ export default useDetectOutsideClick
@@ -0,0 +1,44 @@
1
+ import { getComponentTheme, useTheme } from '../ThemeProvider'
2
+
3
+ /**
4
+ * Generates a label string for a variant based on the provided key and value.
5
+ *
6
+ * @param {string} key - The name of the variant.
7
+ * @param {*} value - The value of the variant. If it's a string, it will be appended to the key.
8
+ * @returns {string} The formatted variant label (e.g., "color: red" or "size").
9
+ */
10
+ const getVariantLabel = (key, value) => `${key}${typeof value === 'string' ? `: ${value}` : ''}`
11
+
12
+ /**
13
+ * Retrieves the variant options for a given component from the theme.
14
+ *
15
+ * @param {string} componentName - The name of the component to get variants for.
16
+ * @returns {Array<Array>} An array of variant tuples. Each tuple contains:
17
+ * - {string|undefined} The variant key (e.g., 'size', 'color', or undefined for default).
18
+ * - {string|undefined} The variant value (e.g., 'small', 'primary', or undefined for default).
19
+ * - {string} The human-readable label for the variant.
20
+ * Returns [['default', {}]] if no componentName is provided.
21
+ * @throws {Error} If the theme does not define appearances for the given component.
22
+ */
23
+ const useVariants = (componentName) => {
24
+ const theme = useTheme()
25
+ if (!componentName) return [['default', {}]]
26
+
27
+ const { appearances } = getComponentTheme(theme, componentName)
28
+ if (!appearances) {
29
+ throw new Error(
30
+ `Theme ${theme.metadata?.name} does not have any appearances set for ${componentName}`
31
+ )
32
+ }
33
+
34
+ const variants = Object.entries(appearances).reduce(
35
+ (pairs, [key, { values, type } = {}]) =>
36
+ type === 'variant'
37
+ ? [...pairs, ...values.map((value) => [key, value, getVariantLabel(key, value)])]
38
+ : pairs,
39
+ [[undefined, undefined, 'default style']]
40
+ )
41
+ return variants
42
+ }
43
+
44
+ export default useVariants