@telus-uds/components-base 2.3.0 → 2.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 (102) hide show
  1. package/CHANGELOG.md +34 -2
  2. package/lib/A11yInfoProvider/index.js +2 -2
  3. package/lib/Autocomplete/Autocomplete.js +22 -32
  4. package/lib/Autocomplete/Suggestions.js +1 -1
  5. package/lib/BaseProvider/HydrationContext.js +1 -2
  6. package/lib/BaseProvider/index.js +1 -2
  7. package/lib/Button/ButtonDropdown.js +1 -1
  8. package/lib/Card/Card.js +12 -13
  9. package/lib/Card/CardBase.js +1 -1
  10. package/lib/Card/PressableCardBase.js +1 -1
  11. package/lib/CardGroup/CardGroup.js +3 -3
  12. package/lib/Carousel/Carousel.js +5 -6
  13. package/lib/Carousel/CarouselStepTracker/CarouselStepTracker.js +3 -0
  14. package/lib/Carousel/CarouselTabs/CarouselTabs.js +1 -2
  15. package/lib/Carousel/CarouselTabs/CarouselTabsPanel.js +1 -1
  16. package/lib/Carousel/CarouselTabs/CarouselTabsPanelItem.js +1 -1
  17. package/lib/Carousel/CarouselThumbnail.js +1 -1
  18. package/lib/Checkbox/Checkbox.js +1 -1
  19. package/lib/ColourToggle/ColourToggle.js +1 -1
  20. package/lib/ExpandCollapseMini/ExpandCollapseMini.js +77 -0
  21. package/lib/ExpandCollapseMini/ExpandCollapseMiniControl.js +126 -0
  22. package/lib/ExpandCollapseMini/index.js +2 -0
  23. package/lib/Footnote/Footnote.js +4 -4
  24. package/lib/HorizontalScroll/HorizontalScroll.js +1 -2
  25. package/lib/Icon/Icon.js +1 -1
  26. package/lib/Icon/IconText.js +2 -3
  27. package/lib/IconButton/IconButton.js +1 -2
  28. package/lib/InputLabel/InputLabel.js +36 -2
  29. package/lib/InputSupports/InputSupports.js +31 -8
  30. package/lib/InputSupports/dictionary.js +12 -0
  31. package/lib/InputSupports/useInputSupports.js +12 -3
  32. package/lib/Link/LinkBase.js +25 -18
  33. package/lib/List/List.js +1 -2
  34. package/lib/List/ListItemContent.js +1 -1
  35. package/lib/Listbox/Listbox.js +5 -8
  36. package/lib/Listbox/PressableItem.js +4 -4
  37. package/lib/Modal/Modal.js +4 -7
  38. package/lib/MultiSelectFilter/MultiSelectFilter.js +55 -42
  39. package/lib/Notification/Notification.js +15 -13
  40. package/lib/OrderedList/OrderedList.js +2 -3
  41. package/lib/Pagination/usePagination.js +1 -2
  42. package/lib/PriceLockup/utils/renderFootnoteContent.js +2 -2
  43. package/lib/PriceLockup/utils/renderFootnoteLinks.js +2 -2
  44. package/lib/PriceLockup/utils/renderPrice.js +2 -2
  45. package/lib/ProductCard/ProductCard.js +2 -3
  46. package/lib/Progress/ProgressBarBackground.js +2 -2
  47. package/lib/QuickLinksFeature/QuickLinksFeature.js +1 -2
  48. package/lib/Radio/Radio.js +1 -1
  49. package/lib/Search/Search.js +41 -11
  50. package/lib/Select/Picker.js +2 -2
  51. package/lib/Select/Picker.native.js +8 -4
  52. package/lib/Select/constants.js +4 -2
  53. package/lib/StackView/StackWrap.js +1 -4
  54. package/lib/StackView/getStackedContent.js +1 -2
  55. package/lib/StepTracker/StepTracker.js +1 -2
  56. package/lib/TabBar/TabBar.js +1 -1
  57. package/lib/Tabs/Tabs.js +1 -1
  58. package/lib/Tabs/TabsItem.js +2 -2
  59. package/lib/TextInput/TextArea.js +7 -6
  60. package/lib/TextInput/TextInput.js +7 -6
  61. package/lib/TextInput/TextInputBase.js +57 -25
  62. package/lib/ThemeProvider/utils/theme-tokens.js +2 -4
  63. package/lib/Timeline/Timeline.js +1 -2
  64. package/lib/Tooltip/Tooltip.native.js +4 -4
  65. package/lib/Typography/Typography.js +4 -5
  66. package/lib/Validator/Validator.js +9 -14
  67. package/lib/ViewportProvider/useViewportListener.js +1 -1
  68. package/lib/index.js +1 -0
  69. package/lib/utils/children.js +2 -6
  70. package/lib/utils/input.js +1 -1
  71. package/lib/utils/props/componentPropType.js +1 -2
  72. package/lib/utils/props/inputSupportsProps.js +15 -3
  73. package/lib/utils/props/selectSystemProps.js +2 -2
  74. package/lib/utils/ssr-media-query/create-stylesheet/create-stylesheet-mobile.js +1 -1
  75. package/lib/utils/ssr-media-query/create-stylesheet/index.js +2 -3
  76. package/lib/utils/ssr-media-query/utils/inject.js +3 -5
  77. package/lib/utils/useHash.js +1 -4
  78. package/lib/utils/useOverlaidPosition.js +25 -4
  79. package/lib/utils/useScrollBlocking.js +2 -4
  80. package/lib/utils/useSpacingScale.js +2 -2
  81. package/package.json +2 -2
  82. package/src/Carousel/CarouselStepTracker/CarouselStepTracker.jsx +3 -0
  83. package/src/ExpandCollapseMini/ExpandCollapseMini.jsx +76 -0
  84. package/src/ExpandCollapseMini/ExpandCollapseMiniControl.jsx +119 -0
  85. package/src/ExpandCollapseMini/index.js +3 -0
  86. package/src/InputLabel/InputLabel.jsx +39 -2
  87. package/src/InputSupports/InputSupports.jsx +33 -7
  88. package/src/InputSupports/dictionary.js +12 -0
  89. package/src/InputSupports/useInputSupports.js +24 -3
  90. package/src/Link/LinkBase.jsx +25 -18
  91. package/src/Modal/Modal.jsx +1 -1
  92. package/src/MultiSelectFilter/MultiSelectFilter.jsx +55 -27
  93. package/src/Notification/Notification.jsx +9 -3
  94. package/src/Search/Search.jsx +52 -24
  95. package/src/Select/Picker.native.jsx +10 -4
  96. package/src/Select/constants.js +4 -1
  97. package/src/TextInput/TextArea.jsx +12 -5
  98. package/src/TextInput/TextInput.jsx +13 -6
  99. package/src/TextInput/TextInputBase.jsx +57 -10
  100. package/src/index.js +1 -0
  101. package/src/utils/props/inputSupportsProps.js +15 -3
  102. package/src/utils/useOverlaidPosition.js +23 -0
@@ -1,6 +1,8 @@
1
1
  import { useCallback, useEffect, useRef, useState } from 'react';
2
2
  import Dimensions from "react-native-web/dist/exports/Dimensions";
3
3
  import Platform from "react-native-web/dist/exports/Platform";
4
+ import debounce from 'lodash.debounce';
5
+ const DEBOUNCE_DELAY = 100;
4
6
  const adjustHorizontalToFit = (initialOffset, windowWidth, sourceWidth) => {
5
7
  const offset = Math.max(0, initialOffset);
6
8
  const otherEdgeOverflow = Math.max(0, offset + sourceWidth - windowWidth);
@@ -157,12 +159,11 @@ const useOverlaidPosition = _ref3 => {
157
159
  const readyToShow = Boolean(isShown && sourceRef.current);
158
160
  useEffect(() => {
159
161
  const handleDimensionsChange = _ref5 => {
160
- var _sourceRef$current;
161
162
  let {
162
163
  window
163
164
  } = _ref5;
164
165
  const measurementFunction = Platform.OS === 'web' ? 'measureInWindow' : 'measure';
165
- (_sourceRef$current = sourceRef.current) === null || _sourceRef$current === void 0 || _sourceRef$current[measurementFunction]((x, y, width, height) => {
166
+ sourceRef.current?.[measurementFunction]((x, y, width, height) => {
166
167
  setWindowDimensions(window);
167
168
  setSourceLayout({
168
169
  x,
@@ -174,8 +175,7 @@ const useOverlaidPosition = _ref3 => {
174
175
  };
175
176
  let subscription;
176
177
  const unsubscribe = () => {
177
- var _subscription;
178
- if (typeof ((_subscription = subscription) === null || _subscription === void 0 ? void 0 : _subscription.remove) === 'function') {
178
+ if (typeof subscription?.remove === 'function') {
179
179
  // React Native >=0.65.0
180
180
  subscription.remove();
181
181
  } else if (typeof Dimensions.remove === 'function') {
@@ -195,6 +195,27 @@ const useOverlaidPosition = _ref3 => {
195
195
  }
196
196
  return unsubscribe;
197
197
  }, [readyToShow]);
198
+ useEffect(() => {
199
+ if (Platform.OS !== 'web') {
200
+ return undefined;
201
+ }
202
+ const handleScroll = debounce(() => {
203
+ sourceRef.current?.measureInWindow((x, y, width, height) => {
204
+ setWindowDimensions(window);
205
+ setSourceLayout({
206
+ x,
207
+ y,
208
+ width,
209
+ height
210
+ });
211
+ });
212
+ }, DEBOUNCE_DELAY);
213
+ window.addEventListener('scroll', handleScroll);
214
+ return () => {
215
+ window.removeEventListener('scroll', handleScroll);
216
+ handleScroll.cancel();
217
+ };
218
+ }, [sourceRef]);
198
219
  const isReady = Boolean(isShown && sourceLayout && windowDimensions && targetDimensions);
199
220
  const overlaidPosition = isReady ? getOverlaidPosition({
200
221
  sourceLayout,
@@ -1,16 +1,14 @@
1
1
  import { useCallback, useEffect, useRef, useState } from 'react';
2
2
  const addScrollBlocking = (preventScrolling, stopPropagation, ref) => {
3
- var _ref$current;
4
3
  document.body.addEventListener('touchmove', preventScrolling, {
5
4
  passive: false
6
5
  });
7
- (_ref$current = ref.current) === null || _ref$current === void 0 || _ref$current.addEventListener('touchmove', stopPropagation);
6
+ ref.current?.addEventListener('touchmove', stopPropagation);
8
7
  document.body.style.overflow = 'hidden';
9
8
  };
10
9
  const removeScrollBlocking = (preventScrolling, stopPropagation, ref) => {
11
- var _ref$current2;
12
10
  document.body.removeEventListener('touchmove', preventScrolling);
13
- (_ref$current2 = ref.current) === null || _ref$current2 === void 0 || _ref$current2.removeEventListener('touchmove', stopPropagation);
11
+ ref.current?.removeEventListener('touchmove', stopPropagation);
14
12
  document.body.style.overflow = 'inherit';
15
13
  };
16
14
 
@@ -10,7 +10,7 @@ import { resolveResponsiveProp } from './useResponsiveProp';
10
10
  */
11
11
 
12
12
  const resolveSpacingOptions = space => {
13
- if (!(space !== null && space !== void 0 && space.options)) return {};
13
+ if (!space?.options) return {};
14
14
  const {
15
15
  size,
16
16
  variant,
@@ -111,7 +111,7 @@ const useSpacingScale = spaceValue => {
111
111
  overridden,
112
112
  subtract = 0
113
113
  } = resolveSpacingOptions(spaceValue);
114
- const space = !overridden && ((spaceValue === null || spaceValue === void 0 ? void 0 : spaceValue.space) ?? resolveResponsiveProp(spaceValue, viewport, 0));
114
+ const space = !overridden && (spaceValue?.space ?? resolveResponsiveProp(spaceValue, viewport, 0));
115
115
  const {
116
116
  size
117
117
  } = useThemeTokens('spacingScale', tokens, variant, {
package/package.json CHANGED
@@ -13,7 +13,7 @@
13
13
  "@gorhom/portal": "^1.0.14",
14
14
  "@react-native-picker/picker": "^2.9.0",
15
15
  "@telus-uds/system-constants": "^2.0.0",
16
- "@telus-uds/system-theme-tokens": "^3.2.0",
16
+ "@telus-uds/system-theme-tokens": "^3.3.0",
17
17
  "airbnb-prop-types": "^2.16.0",
18
18
  "css-mediaquery": "^0.1.2",
19
19
  "expo-document-picker": "^13.0.1",
@@ -81,6 +81,6 @@
81
81
  "standard-engine": {
82
82
  "skip": true
83
83
  },
84
- "version": "2.3.0",
84
+ "version": "2.5.0",
85
85
  "types": "types/index.d.ts"
86
86
  }
@@ -33,6 +33,9 @@ const CarouselStepTracker = React.forwardRef(
33
33
  const steps = Array.from(Array(totalItems)).map((_, index) => String(index))
34
34
 
35
35
  if (enableDisplayMultipleItemsPerSlide) {
36
+ if (totalItems === 1) {
37
+ return null
38
+ }
36
39
  return (
37
40
  <>
38
41
  <Spacer space={4} />
@@ -0,0 +1,76 @@
1
+ import React from 'react'
2
+ import PropTypes from 'prop-types'
3
+ import ExpandCollapse from '../ExpandCollapse'
4
+ import { getTokensPropType } from '../utils'
5
+ import ExpandCollapseMiniControl from './ExpandCollapseMiniControl'
6
+
7
+ const ExpandCollapseMini = React.forwardRef(
8
+ ({ children, onToggle = () => {}, tokens = {}, nativeID, initialOpen = false, ...rest }, ref) => {
9
+ const expandCollapeMiniPanelId = 'ExpandCollapseMiniPanel'
10
+ const handleChange = (openPanels, event) => {
11
+ if (typeof onToggle === 'function') {
12
+ const isOpen = openPanels.length > 0
13
+ onToggle(event, isOpen)
14
+ }
15
+ }
16
+
17
+ return (
18
+ <ExpandCollapse
19
+ onChange={handleChange}
20
+ tokens={tokens}
21
+ initialOpen={initialOpen ? [expandCollapeMiniPanelId] : []}
22
+ >
23
+ {(expandProps) => (
24
+ <ExpandCollapse.Panel
25
+ {...expandProps}
26
+ panelId={expandCollapeMiniPanelId}
27
+ variant={{ mini: true }}
28
+ controlTokens={{
29
+ // Remove unwanted look and feel from ExpandCollapse(background pressed, focus border and text underline)
30
+ icon: null,
31
+ borderColor: 'transparent',
32
+ textLine: 'none',
33
+ backgroundColor: 'transparent'
34
+ }}
35
+ // TODO refactor
36
+ // eslint-disable-next-line react/no-unstable-nested-components
37
+ control={(pressableState) => (
38
+ <ExpandCollapseMiniControl pressableState={pressableState} {...rest} />
39
+ )}
40
+ controlRef={ref}
41
+ nativeID={nativeID}
42
+ >
43
+ {children}
44
+ </ExpandCollapse.Panel>
45
+ )}
46
+ </ExpandCollapse>
47
+ )
48
+ }
49
+ )
50
+ ExpandCollapseMini.displayName = 'ExpandCollapseMini'
51
+
52
+ ExpandCollapseMini.propTypes = {
53
+ ...ExpandCollapseMiniControl.propTypes,
54
+ /**
55
+ * Function to call on pressing the panel's control, which should open or close the panel.
56
+ */
57
+ onToggle: PropTypes.func,
58
+ /**
59
+ * ID for DOM element on web
60
+ */
61
+ nativeID: PropTypes.string,
62
+ /**
63
+ * Children nodes that can be added
64
+ */
65
+ children: PropTypes.node.isRequired,
66
+ /**
67
+ * Controls if the panel and the content is opened by default on the first load
68
+ */
69
+ initialOpen: PropTypes.bool,
70
+ /**
71
+ * Optional variant object to override the default theme tokens
72
+ */
73
+ tokens: getTokensPropType('ExpandCollapseMini')
74
+ }
75
+
76
+ export default ExpandCollapseMini
@@ -0,0 +1,119 @@
1
+ import React from 'react'
2
+ import PropTypes from 'prop-types'
3
+ import { Platform } from 'react-native'
4
+ import { Link } from '../Link'
5
+ import { useThemeTokens } from '../ThemeProvider'
6
+ import { htmlAttrs, viewProps, selectSystemProps } from '../utils'
7
+
8
+ const [selectProps, selectedSystemPropTypes] = selectSystemProps([htmlAttrs, viewProps])
9
+
10
+ // The ExpandCollapseControl has all the appropriate role, a11y, press handling etc
11
+ // and a more appropriate press area, defer interaction handling to it.
12
+ const presentationOnly = {
13
+ accessibilityRole: null, // Treat as regular flow content with the Control
14
+ pointerEvents: 'none', // Stop RNW from stopping clicks from bubbling to Control
15
+ focusable: false // Stop RNW from setting tabIndex={0}: focus goes to Control only
16
+ }
17
+
18
+ const selectLinkTokens = ({ color, textLine, lineHeight, fontSize }) => ({
19
+ color,
20
+ textLine,
21
+ blockLineHeight: lineHeight,
22
+ blockFontSize: fontSize
23
+ })
24
+
25
+ const ExpandCollapseMiniControl = React.forwardRef(
26
+ (
27
+ {
28
+ pressableState,
29
+ collapseTitle,
30
+ expandTitle = collapseTitle,
31
+ iconPosition = 'right',
32
+ tokens,
33
+ variant = {},
34
+ ...rest
35
+ },
36
+ ref
37
+ ) => {
38
+ const { expanded, hover, focus } = pressableState || {}
39
+ // we only want focus outline when focusing, if user is pressing we don't want the border.
40
+ const { outerBorderColor } = useThemeTokens(
41
+ 'Link',
42
+ {},
43
+ {},
44
+ { focus: Platform.OS !== 'web' ? expanded : focus }
45
+ )
46
+ const { size, icon, ...themeTokens } = useThemeTokens(
47
+ 'ExpandCollapseMiniControl',
48
+ tokens,
49
+ variant,
50
+ { expanded, focus }
51
+ )
52
+
53
+ // Choose hover styles when any part of Control is hoverred
54
+ const appearance = { ...variant, hover }
55
+
56
+ const getTokens = (linkState) => {
57
+ const { hover: linkHover } = linkState || {}
58
+ const isHovered = hover || linkHover
59
+
60
+ if (Platform.OS !== 'web') {
61
+ return { iconTranslateY: -1 }
62
+ }
63
+
64
+ if (isHovered) {
65
+ // Include vertical icon animation on hover alongside built-in Link theme, the size is size4
66
+ return { iconTranslateY: (expanded ? -1 : 1) * size }
67
+ }
68
+
69
+ return {}
70
+ }
71
+
72
+ return (
73
+ <Link
74
+ variant={appearance}
75
+ icon={icon}
76
+ iconPosition={iconPosition}
77
+ tokens={(linkState) => ({
78
+ ...getTokens(linkState),
79
+ ...selectLinkTokens(themeTokens),
80
+ outerBorderColor
81
+ })}
82
+ ref={ref}
83
+ {...presentationOnly}
84
+ {...selectProps(rest)}
85
+ >
86
+ {expanded ? expandTitle : collapseTitle}
87
+ </Link>
88
+ )
89
+ }
90
+ )
91
+
92
+ ExpandCollapseMiniControl.displayName = 'ExpandCollapseMiniControl'
93
+
94
+ ExpandCollapseMiniControl.propTypes = {
95
+ ...selectedSystemPropTypes,
96
+ ...Link.propTypes,
97
+ /**
98
+ * Optional function to call on pressing the panel's control, in addition to opening or closing the panel
99
+ */
100
+ onPress: PropTypes.func,
101
+ /**
102
+ * ExpandCollapseMiniControl title when expanded
103
+ */
104
+ expandTitle: PropTypes.string.isRequired,
105
+ /**
106
+ * ExpandCollapseMiniControl title when collapsed
107
+ */
108
+ collapseTitle: PropTypes.string.isRequired,
109
+ /**
110
+ * React Native's `Pressable`'s state object
111
+ */
112
+ pressableState: PropTypes.object,
113
+ /**
114
+ * Optional variant object to override the default theme tokens
115
+ */
116
+ variant: PropTypes.object
117
+ }
118
+
119
+ export default ExpandCollapseMiniControl
@@ -0,0 +1,3 @@
1
+ import ExpandCollapseMini from './ExpandCollapseMini'
2
+
3
+ export default ExpandCollapseMini
@@ -47,6 +47,8 @@ const InputLabel = React.forwardRef(
47
47
  tooltip,
48
48
  tokens,
49
49
  variant,
50
+ characterCount,
51
+ maxCharacterAllowed,
50
52
  ...rest
51
53
  },
52
54
  ref
@@ -95,12 +97,30 @@ const InputLabel = React.forwardRef(
95
97
  <Tooltip content={tooltip} copy={copy} />
96
98
  </View>
97
99
  )}
100
+ {maxCharacterAllowed && isHintInline && (
101
+ <Text style={[selectHintStyles({ ...themeTokens }), staticStyles.characterCountlabel]}>
102
+ {characterCount}/{maxCharacterAllowed}
103
+ </Text>
104
+ )}
98
105
  </View>
99
- {hint && !isHintInline && (
106
+ {hint && !maxCharacterAllowed && !isHintInline && (
100
107
  <Text style={[selectHintStyles(themeTokens), staticStyles.hintBelow]} nativeID={hintId}>
101
108
  {hint}
102
109
  </Text>
103
110
  )}
111
+ {hint && maxCharacterAllowed && !isHintInline && (
112
+ <View style={staticStyles.container}>
113
+ <Text
114
+ style={[selectHintStyles(themeTokens), staticStyles.flexHintBelow]}
115
+ nativeID={hintId}
116
+ >
117
+ {hint}
118
+ </Text>
119
+ <Text style={[selectHintStyles(themeTokens), staticStyles.characterCountlabel]}>
120
+ {characterCount}/{maxCharacterAllowed}
121
+ </Text>
122
+ </View>
123
+ )}
104
124
  </>
105
125
  )
106
126
  }
@@ -137,6 +157,14 @@ InputLabel.propTypes = {
137
157
  * Content of an optional `Tooltip`. If set, a tooltip button will be shown next to the label.
138
158
  */
139
159
  tooltip: PropTypes.string,
160
+ /**
161
+ * Current number of characterts of an input text.
162
+ */
163
+ characterCount: PropTypes.number,
164
+ /**
165
+ * Max number of characters that allows an input text.
166
+ */
167
+ maxCharacterAllowed: PropTypes.number,
140
168
  tokens: getTokensPropType('InputLabel'),
141
169
  variant: variantProp.propType
142
170
  }
@@ -148,13 +176,22 @@ const staticStyles = StyleSheet.create({
148
176
  flexShrink: 1,
149
177
  flexDirection: 'row'
150
178
  },
179
+ characterCountlabel: {
180
+ marginLeft: 'auto',
181
+ marginTop: 'auto'
182
+ },
151
183
  label: {
152
- flexShrink: 1
184
+ flexShrink: 1,
185
+ alignSelf: 'center'
153
186
  },
154
187
  hintBelow: {
155
188
  flexBasis: '100%',
156
189
  flexShrink: 0
157
190
  },
191
+ flexHintBelow: {
192
+ flexBasis: '100%',
193
+ flexShrink: 1
194
+ },
158
195
  tooltipAlign: {
159
196
  alignSelf: 'center',
160
197
  justifyContent: 'center'
@@ -6,7 +6,8 @@ import Feedback from '../Feedback'
6
6
  import StackView from '../StackView'
7
7
  import { useThemeTokens } from '../ThemeProvider'
8
8
  import useInputSupports from './useInputSupports'
9
- import { getTokensPropType } from '../utils'
9
+ import { getTokensPropType, useCopy } from '../utils'
10
+ import dictionary from './dictionary'
10
11
 
11
12
  const InputSupports = React.forwardRef(
12
13
  (
@@ -21,18 +22,31 @@ const InputSupports = React.forwardRef(
21
22
  feedbackProps = {},
22
23
  tooltip,
23
24
  validation,
25
+ maxCharacterAllowed,
26
+ inputValue,
24
27
  nativeID
25
28
  },
26
29
  ref
27
30
  ) => {
28
31
  const { space } = useThemeTokens('InputSupports')
32
+ const getCopy = useCopy({ dictionary, copy })
33
+
34
+ const maxCharsReachedErrorMessage =
35
+ inputValue?.length > maxCharacterAllowed
36
+ ? getCopy('maxCharsMessage').replace(/%\{charCount\}/g, maxCharacterAllowed)
37
+ : ''
38
+
39
+ const feedbackValidation = inputValue?.length > maxCharacterAllowed ? 'error' : validation
29
40
 
30
41
  const { inputId, hintId, feedbackId, a11yProps } = useInputSupports({
31
42
  feedback,
32
43
  hint,
33
44
  label,
34
45
  validation,
35
- nativeID
46
+ nativeID,
47
+ copy,
48
+ maxCharacterAllowed,
49
+ charactersCount: maxCharacterAllowed - inputValue?.length
36
50
  })
37
51
 
38
52
  return (
@@ -46,14 +60,18 @@ const InputSupports = React.forwardRef(
46
60
  hintId={hintId}
47
61
  tooltip={tooltip}
48
62
  forId={inputId}
63
+ characterCount={inputValue?.length}
64
+ maxCharacterAllowed={maxCharacterAllowed}
49
65
  />
50
66
  )}
51
- {typeof children === 'function' ? children({ inputId, ...a11yProps }) : children}
52
- {feedback ? (
67
+ {typeof children === 'function'
68
+ ? children({ inputId, ...a11yProps, validation: feedbackValidation })
69
+ : children}
70
+ {feedback || maxCharsReachedErrorMessage ? (
53
71
  <Feedback
54
72
  id={feedbackId}
55
- title={feedback}
56
- validation={validation}
73
+ title={feedback || maxCharsReachedErrorMessage}
74
+ validation={feedbackValidation}
57
75
  tokens={feedbackTokens}
58
76
  variant={{ icon: feedbackProps.showFeedbackIcon }}
59
77
  {...feedbackProps}
@@ -107,7 +125,15 @@ InputSupports.propTypes = {
107
125
  /**
108
126
  * ID for DOM element on web
109
127
  */
110
- nativeID: PropTypes.string
128
+ nativeID: PropTypes.string,
129
+ /**
130
+ * The text value of a TextArea or TextInput
131
+ */
132
+ inputValue: PropTypes.string,
133
+ /**
134
+ * Max number of characters that allows an input text.
135
+ */
136
+ maxCharacterAllowed: PropTypes.number
111
137
  }
112
138
 
113
139
  export default InputSupports
@@ -0,0 +1,12 @@
1
+ export default {
2
+ en: {
3
+ maxCharacters: 'Maximum %{charCount} characters',
4
+ charactersRemaining: '%{charCount} characters remaining',
5
+ maxCharsMessage: 'Must not exceed %{charCount} characters'
6
+ },
7
+ fr: {
8
+ maxCharacters: '%{charCount} caractères maximum',
9
+ charactersRemaining: '%{charCount} caractères restants',
10
+ maxCharsMessage: 'Ne doit pas dépasser %{charCount} caractères'
11
+ }
12
+ }
@@ -1,8 +1,20 @@
1
1
  import useUniqueId from '../utils/useUniqueId'
2
+ import { useCopy } from '../utils'
3
+ import dictionary from './dictionary'
2
4
 
3
5
  const joinDefined = (array) => array.filter((item) => item !== undefined).join(' ')
4
6
 
5
- const useInputSupports = ({ label, feedback, validation, hint, nativeID }) => {
7
+ const useInputSupports = ({
8
+ label,
9
+ feedback,
10
+ validation,
11
+ hint,
12
+ nativeID,
13
+ copy,
14
+ maxCharacterAllowed,
15
+ charactersCount
16
+ }) => {
17
+ const getCopy = useCopy({ dictionary, copy })
6
18
  const hasValidationError = validation === 'error'
7
19
 
8
20
  const inputId = useUniqueId('input')
@@ -11,10 +23,19 @@ const useInputSupports = ({ label, feedback, validation, hint, nativeID }) => {
11
23
 
12
24
  const a11yProps = {
13
25
  accessibilityLabel: label,
14
- accessibilityHint: joinDefined([!hasValidationError && feedback, hint]), // native only -> replaced with describedBy on web
26
+ accessibilityHint: joinDefined([
27
+ !hasValidationError && feedback,
28
+ hint,
29
+ maxCharacterAllowed
30
+ ? getCopy('maxCharacters').replace(/%\{charCount\}/g, maxCharacterAllowed)
31
+ : undefined
32
+ ]), // native only -> replaced with describedBy on web
15
33
  accessibilityDescribedBy: joinDefined([
16
34
  !hasValidationError && feedback && feedbackId, // feedback receives a11yRole=alert on error, so there's no need to include it here
17
- hint && hintId
35
+ hint && hintId,
36
+ charactersCount
37
+ ? getCopy('charactersRemaining').replace(/%\{charCount\}/g, charactersCount)
38
+ : undefined
18
39
  ]),
19
40
  accessibilityInvalid: hasValidationError
20
41
  }
@@ -32,21 +32,15 @@ const selectOuterBorderStyles = ({
32
32
  outerBorderGap,
33
33
  borderRadius,
34
34
  outerBorderOutline
35
- }) =>
36
- // A view wrapper with a border on native messes up inline text alignment
37
- // so for now make focus styles strictly web-only
38
- Platform.OS === 'web'
39
- ? {
40
- // Allow theme to define outline, or, turn off outline and use border if rounded corners required
41
- outline: outerBorderOutline,
42
- ...applyOuterBorder({
43
- outerBorderColor,
44
- outerBorderWidth,
45
- outerBorderGap
46
- }),
47
- borderRadius
48
- }
49
- : {}
35
+ }) => ({
36
+ outline: outerBorderOutline,
37
+ ...applyOuterBorder({
38
+ outerBorderColor,
39
+ outerBorderWidth,
40
+ outerBorderGap
41
+ }),
42
+ borderRadius
43
+ })
50
44
 
51
45
  const selectTextStyles = ({ color, blockFontSize }) => {
52
46
  return {
@@ -87,18 +81,19 @@ const selectDecorationStyles = ({ color, textLine, textLineStyle, alignSelf }) =
87
81
  })
88
82
  })
89
83
 
90
- const selectIconTokens = ({ color, iconSize, blockFontSize, iconTranslateX }) => {
84
+ const selectIconTokens = ({ color, iconSize, blockFontSize, iconTranslateX, iconTranslateY }) => {
91
85
  /**
92
86
  * These calculations were carried out using a set of linear equations to calculate that the
93
87
  * position of the icon "->"" is aligned to the first line of the tooltip text.
94
88
  * The base equation is: X/4 + Y/4 - 4 - |X - Y| = Z
95
89
  * where X = blockFontSize, Y = iconSize and Z = translateY
96
90
  */
97
- const translateY = blockFontSize / 4 + iconSize / 4 - 4 - Math.abs(iconSize - blockFontSize)
91
+ const translateY =
92
+ iconTranslateY ?? blockFontSize / 4 + iconSize / 4 - 4 - Math.abs(iconSize - blockFontSize)
98
93
  return {
99
94
  color,
100
95
  translateX: iconTranslateX,
101
- translateY: translateY < 0 ? 0 : translateY,
96
+ translateY,
102
97
  size: iconSize
103
98
  }
104
99
  }
@@ -174,6 +169,7 @@ const LinkBase = React.forwardRef(
174
169
  const decorationStyles = selectDecorationStyles(themeTokens)
175
170
  return [
176
171
  outerBorderStyles,
172
+ staticStyles.outerBorderStyles,
177
173
  blockLeftStyle,
178
174
  decorationStyles,
179
175
  hasIcon && staticStyles.rowContainer
@@ -273,6 +269,17 @@ const staticStyles = StyleSheet.create({
273
269
  pointerEvents: 'none'
274
270
  }
275
271
  })
272
+ },
273
+ outerBorderStyles: {
274
+ ...(Platform.OS !== 'web' && {
275
+ margin: 0,
276
+ marginHorizontal: 2,
277
+ padding: 0
278
+ }),
279
+ ...(Platform.OS === 'android' && {
280
+ paddingHorizontal: 2,
281
+ paddingTop: 2
282
+ })
276
283
  }
277
284
  })
278
285
 
@@ -149,7 +149,7 @@ const Modal = React.forwardRef(
149
149
  a[href], button, textarea, input, select,
150
150
  [tabindex]:not([tabindex="-1"]),
151
151
  [contenteditable="true"]
152
- `)
152
+ `) || []
153
153
  )
154
154
 
155
155
  const firstElement = focusableElements[0]