@telus-uds/components-base 3.7.1 → 3.9.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 +34 -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/ExpandCollapseMini/ExpandCollapseMiniControl.js +1 -1
  11. package/lib/cjs/List/ListItemMark.js +13 -2
  12. package/lib/cjs/MultiSelectFilter/ModalOverlay.js +19 -5
  13. package/lib/cjs/MultiSelectFilter/MultiSelectFilter.js +22 -9
  14. package/lib/cjs/ToggleSwitch/ToggleSwitch.js +13 -2
  15. package/lib/cjs/Validator/Validator.js +135 -64
  16. package/lib/cjs/utils/index.js +9 -1
  17. package/lib/cjs/utils/useDetectOutsideClick.js +39 -0
  18. package/lib/cjs/utils/useVariants.js +46 -0
  19. package/lib/esm/ActivityIndicator/FullScreenIndicator.js +82 -0
  20. package/lib/esm/ActivityIndicator/InlineIndicator.js +57 -0
  21. package/lib/esm/ActivityIndicator/OverlayIndicator.js +149 -0
  22. package/lib/esm/ActivityIndicator/RenderActivityIndicator.js +83 -0
  23. package/lib/esm/ActivityIndicator/index.js +89 -23
  24. package/lib/esm/ActivityIndicator/shared.js +11 -0
  25. package/lib/esm/ActivityIndicator/sharedProptypes.js +61 -0
  26. package/lib/esm/Card/Card.js +38 -45
  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 +19 -5
  30. package/lib/esm/MultiSelectFilter/MultiSelectFilter.js +22 -9
  31. package/lib/esm/ToggleSwitch/ToggleSwitch.js +13 -2
  32. package/lib/esm/Validator/Validator.js +135 -64
  33. package/lib/esm/utils/index.js +2 -1
  34. package/lib/esm/utils/useDetectOutsideClick.js +31 -0
  35. package/lib/esm/utils/useVariants.js +41 -0
  36. package/lib/package.json +2 -2
  37. package/package.json +2 -2
  38. package/src/ActivityIndicator/FullScreenIndicator.jsx +65 -0
  39. package/src/ActivityIndicator/InlineIndicator.jsx +47 -0
  40. package/src/ActivityIndicator/OverlayIndicator.jsx +140 -0
  41. package/src/ActivityIndicator/RenderActivityIndicator.jsx +82 -0
  42. package/src/ActivityIndicator/index.jsx +113 -32
  43. package/src/ActivityIndicator/shared.js +11 -0
  44. package/src/ActivityIndicator/sharedProptypes.js +62 -0
  45. package/src/Card/Card.jsx +51 -54
  46. package/src/ExpandCollapseMini/ExpandCollapseMiniControl.jsx +1 -1
  47. package/src/List/ListItemMark.jsx +18 -2
  48. package/src/MultiSelectFilter/ModalOverlay.jsx +18 -5
  49. package/src/MultiSelectFilter/MultiSelectFilter.jsx +21 -10
  50. package/src/ToggleSwitch/ToggleSwitch.jsx +17 -2
  51. package/src/Validator/Validator.jsx +172 -85
  52. package/src/utils/index.js +1 -0
  53. package/src/utils/useDetectOutsideClick.js +35 -0
  54. package/src/utils/useVariants.js +44 -0
@@ -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
@@ -138,6 +139,7 @@ const MultiSelectFilter = React.forwardRef(
138
139
  buttonBackgroundColor,
139
140
  iconColorSelected,
140
141
  buttonBackgroundColorSelected,
142
+ containerBorderColor,
141
143
  ...restTokens
142
144
  } = useThemeTokens(
143
145
  'MultiSelectFilter',
@@ -269,7 +271,7 @@ const MultiSelectFilter = React.forwardRef(
269
271
  <Row>
270
272
  <View>
271
273
  <Typography tokens={{ ...headerStyles, lineHeight: headerLineHeight }}>
272
- {getCopy('filterByLabel').replace(/%\{filterCategory\}/g, label.toLowerCase())}
274
+ {getCopy('filterByLabel').replace(/%\{filterCategory\}/g, label)}
273
275
  </Typography>
274
276
  </View>
275
277
  </Row>
@@ -285,7 +287,7 @@ const MultiSelectFilter = React.forwardRef(
285
287
  )}
286
288
  <Spacer space={4} />
287
289
  <View style={styles.options}>
288
- <ScrollView onLayout={handleScrollViewLayout}>
290
+ <ScrollView onLayout={handleScrollViewLayout} style={styles.scrollContainer}>
289
291
  <Row distribute="between" onLayout={handleRowLayout}>
290
292
  {[...Array(colSize).keys()].map((i) => (
291
293
  <Col xs={TOTAL_COLUMNS / colSize} key={i}>
@@ -305,16 +307,14 @@ const MultiSelectFilter = React.forwardRef(
305
307
 
306
308
  const controlsContent = (
307
309
  <>
308
- {isScrolling ? (
310
+ {isScrolling && (
309
311
  <Divider
310
312
  tokens={{
311
313
  color: dividerColor
312
314
  }}
313
- space={4}
314
315
  />
315
- ) : (
316
- <Spacer space={4} />
317
316
  )}
317
+ <Spacer space={4} />
318
318
  <View style={selectControlsTokens(restTokens)}>
319
319
  <Row horizontalAlign={buttonDirection === 'column' ? 'center' : 'start'}>
320
320
  <StackView
@@ -367,7 +367,8 @@ const MultiSelectFilter = React.forwardRef(
367
367
  minWidth={windowWidth}
368
368
  tokens={{
369
369
  ...tokens,
370
- maxWidth: true
370
+ maxWidth: true,
371
+ borderColor: containerBorderColor
371
372
  }}
372
373
  copy={copy}
373
374
  isReady={isReady}
@@ -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}
@@ -395,7 +397,8 @@ const MultiSelectFilter = React.forwardRef(
395
397
  minWidth={minWidth}
396
398
  tokens={{
397
399
  ...tokens,
398
- maxWidth: items.length > MAX_ITEMS_THRESHOLD ? true : maxWidth
400
+ maxWidth: items.length > MAX_ITEMS_THRESHOLD ? true : maxWidth,
401
+ borderColor: containerBorderColor
399
402
  }}
400
403
  copy={copy}
401
404
  isReady={isReady}
@@ -428,6 +431,9 @@ const styles = StyleSheet.create({
428
431
  },
429
432
  options: {
430
433
  flex: 1
434
+ },
435
+ scrollContainer: {
436
+ padding: 1
431
437
  }
432
438
  })
433
439
 
@@ -539,7 +545,12 @@ MultiSelectFilter.propTypes = {
539
545
  * Sets the maximum number of items in one column. If number of items are more
540
546
  * than the `rowLimit`, they will be rendered in 2 columns.
541
547
  */
542
- rowLimit: PropTypes.number
548
+ rowLimit: PropTypes.number,
549
+ /**
550
+ * If true, clicking outside the content will trigger the a close callback, dismissing the content.
551
+ * @deprecated This parameter will be removed in the next major release; detection will be always enabled by default.
552
+ */
553
+ dismissWhenPressedOutside: PropTypes.bool
543
554
  }
544
555
 
545
556
  export default MultiSelectFilter
@@ -77,6 +77,18 @@ const selectSwitchStyles = ({
77
77
  })
78
78
  })
79
79
 
80
+ const selectMobileTokens = (tokens) => ({
81
+ ...Platform.select({
82
+ web: {},
83
+ default: {
84
+ switchSize: tokens?.mobileSwitchSize,
85
+ trackHeight: tokens?.mobileTrackHeight,
86
+ width: tokens?.mobileWidth,
87
+ trackBorderWidth: tokens?.mobileTrackBorderWidth
88
+ }
89
+ })
90
+ })
91
+
80
92
  const selectLabelStyles = ({ labelMarginLeft }) => ({ marginLeft: labelMarginLeft })
81
93
  const selectLabelTokens = ({
82
94
  labelColor,
@@ -123,7 +135,10 @@ const ToggleSwitch = React.forwardRef(
123
135
 
124
136
  const handlePress = (event) => setValue(!currentValue, event)
125
137
  const getButtonTokens = (buttonState) =>
126
- selectButtonTokens(getTokens(buttonState), getTokens(themeTokens))
138
+ selectButtonTokens(
139
+ getTokens(buttonState, selectMobileTokens(themeTokens)),
140
+ getTokens(themeTokens, selectMobileTokens(themeTokens))
141
+ )
127
142
  const uniqueId = useUniqueId('toggleSwitch')
128
143
  const inputId = id ?? uniqueId
129
144
 
@@ -157,7 +172,7 @@ const ToggleSwitch = React.forwardRef(
157
172
  {...selectProps(rest)}
158
173
  >
159
174
  {(buttonState) => {
160
- const stateTokens = getTokens(buttonState)
175
+ const stateTokens = getTokens(buttonState, selectMobileTokens(themeTokens))
161
176
  const IconComponent = stateTokens.icon
162
177
  const switchStyles = selectSwitchStyles(stateTokens)
163
178
  const trackStyles = selectTrackStyles(stateTokens)
@@ -24,7 +24,7 @@ const Validator = React.forwardRef(
24
24
 
25
25
  const { supportsProps } = selectProps(rest)
26
26
  const strValidation = supportsProps.validation
27
- const [individualCodes, setIndividualCodes] = React.useState({})
27
+ const [, setIndividualCodes] = React.useState({})
28
28
  const [text, setText] = React.useState(value)
29
29
  const validatorsLength = 6
30
30
  const prefix = 'code'
@@ -44,75 +44,110 @@ const Validator = React.forwardRef(
44
44
  const [codeReferences, singleCodes] = React.useMemo(() => {
45
45
  const codes = []
46
46
  const valueCodes = {}
47
- for (let i = 0; validatorsLength && i < validatorsLength; i += 1) {
47
+ Array.from({ length: validatorsLength }, (_, i) => {
48
48
  codes[prefix + i] = React.createRef()
49
49
  valueCodes[prefix + i] = ''
50
50
  valueCodes[prefix + i + sufixValidation] = ''
51
- }
52
- return [codes, valueCodes]
53
- }, [])
54
-
55
- const handleSingleCodes = (codeId, val, validation) => {
56
- singleCodes[codeId] = val
57
- singleCodes[codeId + sufixValidation] = validation
58
- /* eslint-disable no-unused-expressions */
59
- setIndividualCodes({
60
- ...individualCodes,
61
- [codeId]: val
51
+ return null
62
52
  })
63
- }
64
-
65
- const changeDataMasking = (boxElement) => {
66
- let charMasking = ''
67
- const element = boxElement
68
- if (mask && mask.length === 1) charMasking = mask
69
- else if (mask && mask.length > 1) charMasking = mask.substring(0, 1)
53
+ return [codes, valueCodes]
54
+ }, [validatorsLength, prefix, sufixValidation])
70
55
 
71
- if (charMasking) element.value = charMasking
72
- }
56
+ const handleSingleCodes = React.useCallback(
57
+ (codeId, val, validation) => {
58
+ singleCodes[codeId] = val
59
+ singleCodes[codeId + sufixValidation] = validation
60
+ setIndividualCodes((prev) => ({
61
+ ...prev,
62
+ [codeId]: val
63
+ }))
64
+ },
65
+ [singleCodes, sufixValidation]
66
+ )
73
67
 
74
- const handleChangeCode = () => {
75
- let code = ''
76
- for (let i = 0; i < validatorsLength; i += 1) code += singleCodes[prefix + i]
77
- if (typeof onChange === 'function') onChange(code, singleCodes)
78
- }
68
+ const changeDataMasking = React.useCallback(
69
+ (boxElement) => {
70
+ let charMasking = ''
71
+ const element = boxElement
72
+ if (mask && mask.length === 1) {
73
+ charMasking = mask
74
+ } else if (mask && mask.length > 1) {
75
+ charMasking = mask.substring(0, 1)
76
+ }
79
77
 
80
- const handleChangeCodeValues = (event, codeId, nextIndex) => {
81
- const codeElement = codeReferences[codeId]?.current
82
- const val = event.nativeEvent?.value || event.target?.value
78
+ if (charMasking && element) {
79
+ element.value = charMasking
80
+ }
81
+ },
82
+ [mask]
83
+ )
83
84
 
84
- if (Number(val).toString() === 'NaN') {
85
- codeElement.value = singleCodes[codeId] ?? ''
86
- return
85
+ const handleChangeCode = React.useCallback(() => {
86
+ const code = Array.from(
87
+ { length: validatorsLength },
88
+ (_, i) => singleCodes[prefix + i] || ''
89
+ ).join('')
90
+ if (typeof onChange === 'function') {
91
+ onChange(code, singleCodes)
87
92
  }
93
+ }, [validatorsLength, singleCodes, prefix, onChange])
88
94
 
89
- handleSingleCodes(codeId, codeElement?.value ?? singleCodes[codeId], 'success')
90
- handleChangeCode()
91
- if (nextIndex === validatorsLength) {
92
- codeElement.blur()
93
- changeDataMasking(codeElement)
94
- return
95
- }
96
- if (codeElement?.value?.length > 0) {
97
- codeReferences[prefix + nextIndex].current.focus()
98
- changeDataMasking(codeElement)
99
- }
100
- }
95
+ const handleChangeCodeValues = React.useCallback(
96
+ (event, codeId, nextIndex) => {
97
+ const codeElement = codeReferences[codeId]?.current
98
+ const val = event.nativeEvent?.value || event.target?.value
99
+
100
+ // Only allow numeric characters and limit to single digit
101
+ const numericOnly = val.replace(/\D/g, '').substring(0, 1)
102
+
103
+ if (codeElement && codeElement.value) {
104
+ codeElement.value = numericOnly
105
+ }
106
+
107
+ handleSingleCodes(codeId, numericOnly, numericOnly ? 'success' : '')
108
+ handleChangeCode()
109
+
110
+ if (nextIndex === validatorsLength) {
111
+ codeElement?.blur()
112
+ changeDataMasking(codeElement)
113
+ return
114
+ }
115
+
116
+ if (numericOnly.length > 0) {
117
+ const nextElement = codeReferences[prefix + nextIndex]?.current
118
+ nextElement?.focus()
119
+ changeDataMasking(codeElement)
120
+ }
121
+ },
122
+ [
123
+ codeReferences,
124
+ handleSingleCodes,
125
+ handleChangeCode,
126
+ validatorsLength,
127
+ changeDataMasking,
128
+ prefix
129
+ ]
130
+ )
101
131
 
102
132
  const handleKeyPress = (event, currentIndex, previousIndex) => {
103
- if (!(event.keyCode === 8 || event.code === 'Backspace')) return
133
+ if (!(event.keyCode === 8 || event.code === 'Backspace')) {
134
+ return
135
+ }
104
136
  if (currentIndex > 0) {
105
- codeReferences[prefix + currentIndex].current.value = ''
106
- codeReferences[prefix + previousIndex].current.focus()
137
+ const currentElement = codeReferences[prefix + currentIndex]?.current
138
+ const previousElement = codeReferences[prefix + previousIndex]?.current
139
+
140
+ if (currentElement && currentElement.value) {
141
+ currentElement.value = ''
142
+ }
143
+ previousElement?.focus()
107
144
  }
108
145
  handleSingleCodes(prefix + currentIndex, '', '')
109
146
  handleChangeCode()
110
147
  }
111
148
 
112
149
  const getCodeComponents = () => {
113
- const components = []
114
-
115
- for (let i = 0; validatorsLength && i < validatorsLength; i += 1) {
150
+ return Array.from({ length: validatorsLength }, (_, i) => {
116
151
  const codeId = prefix + i
117
152
  const codeInputProps = {
118
153
  nativeID: codeId,
@@ -120,75 +155,127 @@ const Validator = React.forwardRef(
120
155
  ref: codeReferences[codeId] ?? null,
121
156
  validation: strValidation || singleCodes[codeId + sufixValidation],
122
157
  tokens: selectCodeTextInputTokens(themeTokens),
123
- onFocus: () => codeReferences[codeId]?.current?.select() ?? null,
158
+ onFocus: () => {
159
+ const element = codeReferences[codeId]?.current
160
+ if (Platform.OS === 'web' && element?.select) {
161
+ return element.select() ?? null
162
+ }
163
+ if (element?.focus) {
164
+ return element.focus() ?? null
165
+ }
166
+ return null
167
+ },
124
168
  onKeyPress: (event) => handleKeyPress(event, i, i - 1),
125
169
  onMouseOver: handleMouseOver,
126
170
  onMouseOut: handleMouseOut,
127
171
  inactive
128
172
  }
129
- codeInputProps.validation || delete codeInputProps.validation
173
+ if (!codeInputProps.validation) {
174
+ delete codeInputProps.validation
175
+ }
130
176
 
131
- components.push(
177
+ return (
132
178
  <View key={codeId} style={staticStyles.codeInputWidth}>
133
179
  <TextInput {...codeInputProps} />
134
180
  </View>
135
181
  )
136
- }
137
- return components
182
+ })
138
183
  }
139
184
 
140
185
  React.useEffect(() => {
141
- /* eslint-disable no-unused-expressions */
142
- if (Number(value).toString() !== 'NaN') setText(value)
186
+ if (Number(value).toString() !== 'NaN') {
187
+ setText(value)
188
+ }
143
189
  }, [value])
144
190
 
145
- /* eslint-disable react-hooks/exhaustive-deps */
146
191
  React.useEffect(() => {
147
- for (let i = 0; i < validatorsLength; i += 1) {
148
- if (mask && text[i]) codeReferences[prefix + i].current.value = mask
149
- else codeReferences[prefix + i].current.value = text[i] ?? ''
192
+ Array.from({ length: validatorsLength }, (_, i) => {
193
+ const element = codeReferences[prefix + i]?.current
194
+ if (element && element.value !== undefined) {
195
+ if (mask && text[i]) {
196
+ element.value = mask
197
+ } else {
198
+ element.value = text[i] ?? ''
199
+ }
200
+ }
150
201
  handleSingleCodes(prefix + i, text[i] ?? '', text[i] ? 'success' : '')
151
- }
152
- }, [text])
202
+ return null
203
+ })
204
+ }, [text, mask, validatorsLength, prefix, codeReferences, handleSingleCodes])
153
205
 
154
- /* eslint-disable react-hooks/exhaustive-deps */
155
206
  React.useEffect(() => {
156
207
  const handlePasteCode = (event) => {
208
+ event.preventDefault()
209
+
210
+ // Clear current state first
157
211
  setText('')
212
+
213
+ // Clear all individual input fields and their state
214
+ Array.from({ length: validatorsLength }, (_, i) => {
215
+ const element = codeReferences[prefix + i]?.current
216
+ if (element && element.value !== undefined) {
217
+ element.value = ''
218
+ }
219
+ handleSingleCodes(prefix + i, '', '')
220
+ return null
221
+ })
222
+
158
223
  const clipBoardText = event.clipboardData.getData('text')
159
- if (Number(clipBoardText).toString() !== 'NaN') setText(clipBoardText)
224
+
225
+ // Validate that input contains only digits and truncate to 6 characters
226
+ const numericOnly = clipBoardText.replace(/\D/g, '').substring(0, validatorsLength)
227
+
228
+ if (numericOnly.length > 0) {
229
+ setText(numericOnly)
230
+ }
160
231
  }
161
232
 
162
233
  const handleCopy = (event) => {
163
- let clipBoardText = ''
164
- for (let i = 0; i < validatorsLength; i += 1)
165
- singleCodes[prefix + i] && (clipBoardText += singleCodes[prefix + i])
234
+ const clipBoardText = Array.from(
235
+ { length: validatorsLength },
236
+ (_, i) => singleCodes[prefix + i] || ''
237
+ ).join('')
166
238
  event.clipboardData.setData('text/plain', clipBoardText)
167
239
  event.preventDefault()
168
240
  }
169
241
 
170
242
  if (Platform.OS === 'web') {
171
- for (let i = 0; i < validatorsLength; i += 1) {
172
- codeReferences[prefix + i].current.addEventListener('paste', handlePasteCode)
173
- codeReferences[prefix + i].current.addEventListener('copy', handleCopy)
174
- codeReferences[prefix + i].current.addEventListener('input', (event) =>
175
- handleChangeCodeValues(event, prefix + i, i + 1)
176
- )
177
- }
178
- }
179
-
180
- return () => {
181
- if (Platform.oldValue === 'web') {
182
- for (let i = 0; i < validatorsLength; i += 1) {
183
- codeReferences[prefix + i]?.current?.removeEventListener('paste', handlePasteCode)
184
- codeReferences[prefix + i]?.current?.removeEventListener('copy', handleCopy)
185
- codeReferences[prefix + i]?.current?.removeEventListener('input', (event) =>
243
+ Array.from({ length: validatorsLength }, (_, i) => {
244
+ const element = codeReferences[prefix + i]?.current
245
+ if (element && typeof element.addEventListener === 'function') {
246
+ element.addEventListener('paste', handlePasteCode)
247
+ element.addEventListener('copy', handleCopy)
248
+ element.addEventListener('input', (event) =>
186
249
  handleChangeCodeValues(event, prefix + i, i + 1)
187
250
  )
188
251
  }
252
+ return null
253
+ })
254
+ }
255
+
256
+ return () => {
257
+ if (Platform.OS === 'web') {
258
+ Array.from({ length: validatorsLength }, (_, i) => {
259
+ const element = codeReferences[prefix + i]?.current
260
+ if (element && typeof element.removeEventListener === 'function') {
261
+ element.removeEventListener('paste', handlePasteCode)
262
+ element.removeEventListener('copy', handleCopy)
263
+ element.removeEventListener('input', (event) =>
264
+ handleChangeCodeValues(event, prefix + i, i + 1)
265
+ )
266
+ }
267
+ return null
268
+ })
189
269
  }
190
270
  }
191
- }, [])
271
+ }, [
272
+ validatorsLength,
273
+ prefix,
274
+ codeReferences,
275
+ handleChangeCodeValues,
276
+ handleSingleCodes,
277
+ singleCodes
278
+ ])
192
279
 
193
280
  return (
194
281
  <InputSupports
@@ -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