@telus-uds/components-base 3.21.0 → 3.23.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.
- package/CHANGELOG.md +31 -1
- package/lib/cjs/Button/Button.js +12 -3
- package/lib/cjs/Button/ButtonBase.js +63 -10
- package/lib/cjs/Button/ButtonDropdown.js +2 -0
- package/lib/cjs/Button/ButtonGroup.js +45 -38
- package/lib/cjs/Button/propTypes.js +6 -0
- package/lib/cjs/Card/PressableCardBase.js +3 -1
- package/lib/cjs/Carousel/Carousel.js +63 -22
- package/lib/cjs/Carousel/CarouselItem/CarouselItem.js +23 -3
- package/lib/cjs/Icon/Icon.js +8 -11
- package/lib/cjs/Icon/IconText.js +0 -1
- package/lib/cjs/Listbox/GroupControl.js +33 -39
- package/lib/cjs/Listbox/Listbox.js +22 -13
- package/lib/cjs/Listbox/ListboxGroup.js +2 -1
- package/lib/cjs/Listbox/ListboxOverlay.js +5 -2
- package/lib/cjs/Listbox/PressableItem.js +8 -4
- package/lib/cjs/TextInput/TextInputBase.js +5 -1
- package/lib/cjs/ThemeProvider/index.js +9 -1
- package/lib/cjs/ThemeProvider/useResponsiveThemeTokensCallback.js +124 -0
- package/lib/cjs/Validator/Validator.js +171 -135
- package/lib/cjs/index.js +7 -0
- package/lib/esm/Button/Button.js +13 -4
- package/lib/esm/Button/ButtonBase.js +64 -11
- package/lib/esm/Button/ButtonDropdown.js +2 -0
- package/lib/esm/Button/ButtonGroup.js +44 -39
- package/lib/esm/Button/propTypes.js +6 -0
- package/lib/esm/Card/PressableCardBase.js +3 -1
- package/lib/esm/Carousel/Carousel.js +63 -22
- package/lib/esm/Carousel/CarouselItem/CarouselItem.js +23 -3
- package/lib/esm/Icon/Icon.js +8 -11
- package/lib/esm/Icon/IconText.js +0 -1
- package/lib/esm/Listbox/GroupControl.js +33 -39
- package/lib/esm/Listbox/Listbox.js +23 -14
- package/lib/esm/Listbox/ListboxGroup.js +2 -1
- package/lib/esm/Listbox/ListboxOverlay.js +5 -2
- package/lib/esm/Listbox/PressableItem.js +8 -4
- package/lib/esm/TextInput/TextInputBase.js +5 -1
- package/lib/esm/ThemeProvider/index.js +1 -0
- package/lib/esm/ThemeProvider/useResponsiveThemeTokensCallback.js +117 -0
- package/lib/esm/Validator/Validator.js +171 -135
- package/lib/esm/index.js +1 -1
- package/lib/package.json +2 -2
- package/package.json +2 -2
- package/src/Button/Button.jsx +26 -5
- package/src/Button/ButtonBase.jsx +79 -16
- package/src/Button/ButtonDropdown.jsx +2 -0
- package/src/Button/ButtonGroup.jsx +62 -45
- package/src/Button/propTypes.js +6 -0
- package/src/Card/PressableCardBase.jsx +3 -1
- package/src/Carousel/Carousel.jsx +71 -7
- package/src/Carousel/CarouselItem/CarouselItem.jsx +31 -3
- package/src/Icon/Icon.jsx +11 -14
- package/src/Icon/IconText.jsx +0 -1
- package/src/Listbox/GroupControl.jsx +41 -47
- package/src/Listbox/Listbox.jsx +26 -9
- package/src/Listbox/ListboxGroup.jsx +2 -1
- package/src/Listbox/ListboxOverlay.jsx +7 -2
- package/src/Listbox/PressableItem.jsx +8 -4
- package/src/PriceLockup/utils/renderPrice.jsx +15 -17
- package/src/TextInput/TextInputBase.jsx +5 -1
- package/src/ThemeProvider/index.js +1 -0
- package/src/ThemeProvider/useResponsiveThemeTokensCallback.js +129 -0
- package/src/Validator/Validator.jsx +180 -159
- package/src/index.js +2 -1
|
@@ -30,12 +30,14 @@ const DropdownOverlay = /*#__PURE__*/React.forwardRef((_ref, ref) => {
|
|
|
30
30
|
maxWidth,
|
|
31
31
|
minWidth,
|
|
32
32
|
onLayout,
|
|
33
|
-
tokens
|
|
33
|
+
tokens,
|
|
34
|
+
testID
|
|
34
35
|
} = _ref;
|
|
35
36
|
const systemTokens = useThemeTokens('Listbox', {}, {});
|
|
36
37
|
return /*#__PURE__*/_jsx(View, {
|
|
37
38
|
ref: ref,
|
|
38
39
|
onLayout: onLayout,
|
|
40
|
+
testID: testID,
|
|
39
41
|
style: [overlaidPosition, {
|
|
40
42
|
maxWidth,
|
|
41
43
|
minWidth
|
|
@@ -74,6 +76,7 @@ DropdownOverlay.propTypes = {
|
|
|
74
76
|
maxWidth: PropTypes.number,
|
|
75
77
|
minWidth: PropTypes.number,
|
|
76
78
|
onLayout: PropTypes.func,
|
|
77
|
-
tokens: PropTypes.object
|
|
79
|
+
tokens: PropTypes.object,
|
|
80
|
+
testID: PropTypes.string
|
|
78
81
|
};
|
|
79
82
|
export default Platform.OS === 'web' ? withPortal(DropdownOverlay) : DropdownOverlay;
|
|
@@ -41,10 +41,14 @@ const getItemStyles = _ref => {
|
|
|
41
41
|
color: itemColor,
|
|
42
42
|
outline: itemOutline,
|
|
43
43
|
textDecoration: itemTextDecoration,
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
44
|
+
borderLeftWidth: itemBorderLeftWidth,
|
|
45
|
+
borderLeftColor: itemBorderLeftColor,
|
|
46
|
+
borderRightWidth: itemBorderRightWidth,
|
|
47
|
+
borderRightColor: itemBorderRightColor,
|
|
48
|
+
borderTopWidth: itemBorderTopWidth,
|
|
49
|
+
borderTopColor: itemBorderTopColor,
|
|
50
|
+
borderBottomWidth: itemBorderBottomWidth,
|
|
51
|
+
borderBottomColor: itemBorderBottomColor,
|
|
48
52
|
borderRadius: itemBorderRadius,
|
|
49
53
|
justifyContent: 'center'
|
|
50
54
|
};
|
|
@@ -279,6 +279,10 @@ const TextInputBase = /*#__PURE__*/React.forwardRef((_ref8, ref) => {
|
|
|
279
279
|
// Add a space every 4 digits starting from the 5th position
|
|
280
280
|
filteredText = formattedValue.replace(regex, '$1 ').trim();
|
|
281
281
|
}
|
|
282
|
+
// Apply maxLength if provided
|
|
283
|
+
if (rest.maxLength && filteredText && filteredText.length > rest.maxLength) {
|
|
284
|
+
filteredText = filteredText.substring(0, rest.maxLength);
|
|
285
|
+
}
|
|
282
286
|
setValue(filteredText, event);
|
|
283
287
|
if (typeof onChangeText === 'function') onChangeText(filteredText, event);
|
|
284
288
|
};
|
|
@@ -351,7 +355,7 @@ const TextInputBase = /*#__PURE__*/React.forwardRef((_ref8, ref) => {
|
|
|
351
355
|
onMouseOut: handleMouseOut,
|
|
352
356
|
onChange: handleChangeText,
|
|
353
357
|
defaultValue: initialValue,
|
|
354
|
-
maxLength: type === 'card' ? 19 :
|
|
358
|
+
maxLength: type === 'card' ? 19 : rest.maxLength,
|
|
355
359
|
value: isControlled ? currentValue : undefined,
|
|
356
360
|
onKeyPress
|
|
357
361
|
};
|
|
@@ -2,6 +2,7 @@ import ThemeProvider from './ThemeProvider';
|
|
|
2
2
|
export { default as useTheme } from './useTheme';
|
|
3
3
|
export { default as useSetTheme } from './useSetTheme';
|
|
4
4
|
export { default as useResponsiveThemeTokens } from './useResponsiveThemeTokens';
|
|
5
|
+
export { default as useResponsiveThemeTokensCallback } from './useResponsiveThemeTokensCallback';
|
|
5
6
|
export * from './useThemeTokens';
|
|
6
7
|
export * from './utils';
|
|
7
8
|
export default ThemeProvider;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { useCallback } from 'react';
|
|
2
|
+
import { viewports } from '@telus-uds/system-constants';
|
|
3
|
+
import useTheme from './useTheme';
|
|
4
|
+
import { getComponentTheme, mergeAppearances, resolveThemeTokens } from './utils';
|
|
5
|
+
const getResponsiveThemeTokens = function (_ref, tokensProp) {
|
|
6
|
+
let {
|
|
7
|
+
rules = [],
|
|
8
|
+
tokens: defaultThemeTokens = {}
|
|
9
|
+
} = _ref;
|
|
10
|
+
let variants = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
|
|
11
|
+
let states = arguments.length > 3 ? arguments[3] : undefined;
|
|
12
|
+
const appearances = mergeAppearances(variants, states);
|
|
13
|
+
const tokensByViewport = Object.fromEntries(viewports.keys.map(viewport => [viewport, {
|
|
14
|
+
...defaultThemeTokens
|
|
15
|
+
}]));
|
|
16
|
+
|
|
17
|
+
// Go through each rule and collect them for the corresponding viewport if they apply
|
|
18
|
+
rules.forEach(rule => {
|
|
19
|
+
if (doesRuleApply(rule, appearances)) {
|
|
20
|
+
// If the rule does not have a viewport specified, we collect it in all viewports
|
|
21
|
+
let targetViewports = rule.if.viewport || viewports.keys;
|
|
22
|
+
if (!Array.isArray(targetViewports)) {
|
|
23
|
+
targetViewports = [targetViewports];
|
|
24
|
+
}
|
|
25
|
+
targetViewports.forEach(viewport => {
|
|
26
|
+
tokensByViewport[viewport] = {
|
|
27
|
+
...tokensByViewport[viewport],
|
|
28
|
+
...rule.tokens
|
|
29
|
+
};
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
Object.keys(tokensByViewport).forEach(viewport => {
|
|
34
|
+
tokensByViewport[viewport] = resolveThemeTokens(tokensByViewport[viewport], appearances, tokensProp);
|
|
35
|
+
});
|
|
36
|
+
return tokensByViewport;
|
|
37
|
+
};
|
|
38
|
+
const doesRuleApply = (rule, appearances) => Object.entries(rule.if).every(condition => doesConditionApply(condition, appearances));
|
|
39
|
+
const doesConditionApply = (_ref2, appearances) => {
|
|
40
|
+
let [key, value] = _ref2;
|
|
41
|
+
if (key === 'viewport') {
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
// use null rather than undefined so we can serialise the value in themes
|
|
45
|
+
const appearanceValue = appearances[key] ?? null;
|
|
46
|
+
return Array.isArray(value) ? value.includes(appearanceValue) : value === appearanceValue;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @typedef {import('../utils/props/tokens.js').TokensSet} TokensSet
|
|
51
|
+
* @typedef {import('../utils/props/tokens.js').TokensProp} TokensProp
|
|
52
|
+
* @typedef {import('../utils/props/variantProp').AppearanceSet} AppearanceSet
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Returns a memoised tokens getter function that gets responsive tokens for all viewports,
|
|
57
|
+
* similar to calling useResponsiveThemeTokens but with the callback pattern of useThemeTokensCallback.
|
|
58
|
+
*
|
|
59
|
+
* Scenarios where `useResponsiveThemeTokensCallback` should be used:
|
|
60
|
+
*
|
|
61
|
+
* - Where responsive tokens are to be obtained from state that is accessible only in scopes like callbacks
|
|
62
|
+
* and render functions, where calling useResponsiveThemeTokens directly would be disallowed by React's hook rules.
|
|
63
|
+
* - When using media query stylesheets and need to resolve tokens based on dynamic state (e.g., pressed, hovered)
|
|
64
|
+
* that changes at runtime.
|
|
65
|
+
* - Passing a responsive tokens getter down via a child component's `tokens` prop, applying rules using the
|
|
66
|
+
* child component's current state.
|
|
67
|
+
*
|
|
68
|
+
* The function returned may be called with an object of state appearances to get an object
|
|
69
|
+
* of tokens for each viewport, which can then be passed to createMediaQueryStyles.
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* // Resolving responsive tokens inside Pressable's style function, based on Pressable state
|
|
73
|
+
* const PressMe = ({ tokens, variant, children }) => {
|
|
74
|
+
* const getResponsiveTokens = useResponsiveThemeTokensCallback('PressMe', tokens, variant)
|
|
75
|
+
* const getPressableStyle = ({ pressed }) => {
|
|
76
|
+
* const responsiveTokens = getResponsiveTokens({ pressed })
|
|
77
|
+
* const mediaQueryStyles = createMediaQueryStyles(responsiveTokens)
|
|
78
|
+
* return mediaQueryStyles
|
|
79
|
+
* }
|
|
80
|
+
* return <Pressable style={getPressableStyle}>{children}</Pressable>
|
|
81
|
+
* }
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* // Setting the theme in a parent and resolving it in a child based on child's state
|
|
85
|
+
* const MenuButton = ({ tokens, variant, ...buttonProps }) => {
|
|
86
|
+
* // Define what theme, variant etc we want in this component...
|
|
87
|
+
* const getResponsiveTokens = useResponsiveThemeTokensCallback('Button', tokens, variant)
|
|
88
|
+
* // ...resolve them in another component based on its state (e.g. press, hover...)
|
|
89
|
+
* return <ButtonBase tokens={getResponsiveTokens} accessibilityRole="menuitem" {...buttonProps} />
|
|
90
|
+
* }
|
|
91
|
+
*
|
|
92
|
+
* @typedef {Object} ResponsiveObject
|
|
93
|
+
* @property {TokensSet} xs
|
|
94
|
+
* @property {TokensSet} sm
|
|
95
|
+
* @property {TokensSet} md
|
|
96
|
+
* @property {TokensSet} lg
|
|
97
|
+
* @property {TokensSet} xl
|
|
98
|
+
*
|
|
99
|
+
* @param {string} componentName - the name as defined in the theme schema of the component whose theme is to be used
|
|
100
|
+
* @param {TokensProp} [tokens] - every themed component should accept a `tokens` prop allowing theme tokens to be overridden
|
|
101
|
+
* @param {AppearanceSet} [variants] - variants passed in as props that don't change dynamically
|
|
102
|
+
* @returns {(states: AppearanceSet, tokenOverrides?: TokensProp) => ResponsiveObject}
|
|
103
|
+
* - callback function that returns an overridable responsive tokens object for current state. Only pass
|
|
104
|
+
* tokenOverrides in rare cases where tokens overrides are also generated outside hook scope.
|
|
105
|
+
*/
|
|
106
|
+
const useResponsiveThemeTokensCallback = function (componentName) {
|
|
107
|
+
let tokens = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
|
|
108
|
+
let variants = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
|
|
109
|
+
const theme = useTheme();
|
|
110
|
+
const componentTheme = getComponentTheme(theme, componentName);
|
|
111
|
+
const getResponsiveThemeTokensCallback = useCallback((states, tokenOverrides) => {
|
|
112
|
+
const resolvedTokens = resolveThemeTokens(tokens, mergeAppearances(variants, states), tokenOverrides);
|
|
113
|
+
return getResponsiveThemeTokens(componentTheme, resolvedTokens, variants, states);
|
|
114
|
+
}, [componentTheme, tokens, variants]);
|
|
115
|
+
return getResponsiveThemeTokensCallback;
|
|
116
|
+
};
|
|
117
|
+
export default useResponsiveThemeTokensCallback;
|
|
@@ -37,11 +37,10 @@ const Validator = /*#__PURE__*/React.forwardRef((_ref2, ref) => {
|
|
|
37
37
|
supportsProps
|
|
38
38
|
} = selectProps(rest);
|
|
39
39
|
const strValidation = supportsProps.validation;
|
|
40
|
-
const [, setIndividualCodes] = React.useState({});
|
|
41
|
-
const [text, setText] = React.useState(value);
|
|
42
40
|
const validatorsLength = 6;
|
|
43
41
|
const prefix = 'code';
|
|
44
42
|
const sufixValidation = 'Validation';
|
|
43
|
+
const [codes, setCodes] = React.useState(() => Array(validatorsLength).fill(''));
|
|
45
44
|
const [isHover, setIsHover] = React.useState(false);
|
|
46
45
|
const handleMouseOver = () => {
|
|
47
46
|
setIsHover(true);
|
|
@@ -52,84 +51,87 @@ const Validator = /*#__PURE__*/React.forwardRef((_ref2, ref) => {
|
|
|
52
51
|
const themeTokens = useThemeTokens('TextInput', tokens, variant, {
|
|
53
52
|
hover: isHover
|
|
54
53
|
});
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
Array.from({
|
|
54
|
+
|
|
55
|
+
// Create refs for input elements
|
|
56
|
+
const codeReferences = React.useMemo(() => {
|
|
57
|
+
return Array.from({
|
|
59
58
|
length: validatorsLength
|
|
60
|
-
}, (
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
59
|
+
}, () => /*#__PURE__*/React.createRef());
|
|
60
|
+
}, [validatorsLength]);
|
|
61
|
+
|
|
62
|
+
// Keep onChange and mask in refs to avoid re-creating event listeners
|
|
63
|
+
const onChangeRef = React.useRef(onChange);
|
|
64
|
+
const maskRef = React.useRef(mask);
|
|
65
|
+
React.useEffect(() => {
|
|
66
|
+
onChangeRef.current = onChange;
|
|
67
|
+
maskRef.current = mask;
|
|
68
|
+
}, [onChange, mask]);
|
|
69
|
+
|
|
70
|
+
// Update a single code digit
|
|
71
|
+
const updateCode = React.useCallback((index, digit) => {
|
|
72
|
+
setCodes(prevCodes => {
|
|
73
|
+
const newCodes = [...prevCodes];
|
|
74
|
+
newCodes[index] = digit;
|
|
75
|
+
if (onChangeRef.current) {
|
|
76
|
+
const codeString = newCodes.join('');
|
|
77
|
+
const singleCodesObj = {};
|
|
78
|
+
newCodes.forEach((code, i) => {
|
|
79
|
+
singleCodesObj[prefix + i] = code;
|
|
80
|
+
singleCodesObj[prefix + i + sufixValidation] = code ? 'success' : '';
|
|
81
|
+
});
|
|
82
|
+
onChangeRef.current(codeString, singleCodesObj);
|
|
83
|
+
}
|
|
84
|
+
return newCodes;
|
|
65
85
|
});
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
singleCodes[codeId + sufixValidation] = validation;
|
|
71
|
-
setIndividualCodes(prev => ({
|
|
72
|
-
...prev,
|
|
73
|
-
[codeId]: val
|
|
74
|
-
}));
|
|
75
|
-
}, [singleCodes, sufixValidation]);
|
|
76
|
-
const changeDataMasking = React.useCallback(boxElement => {
|
|
77
|
-
let charMasking = '';
|
|
78
|
-
const element = boxElement;
|
|
79
|
-
if (mask && mask.length === 1) {
|
|
80
|
-
charMasking = mask;
|
|
81
|
-
} else if (mask && mask.length > 1) {
|
|
82
|
-
charMasking = mask.substring(0, 1);
|
|
83
|
-
}
|
|
84
|
-
if (charMasking && element) {
|
|
85
|
-
element.value = charMasking;
|
|
86
|
-
}
|
|
87
|
-
}, [mask]);
|
|
88
|
-
const handleChangeCode = React.useCallback(() => {
|
|
89
|
-
const code = Array.from({
|
|
90
|
-
length: validatorsLength
|
|
91
|
-
}, (_, i) => singleCodes[prefix + i] || '').join('');
|
|
92
|
-
if (typeof onChange === 'function') {
|
|
93
|
-
onChange(code, singleCodes);
|
|
94
|
-
}
|
|
95
|
-
}, [validatorsLength, singleCodes, prefix, onChange]);
|
|
96
|
-
const handleChangeCodeValues = React.useCallback((event, codeId, nextIndex) => {
|
|
97
|
-
const codeElement = codeReferences[codeId]?.current;
|
|
86
|
+
}, [prefix, sufixValidation]);
|
|
87
|
+
|
|
88
|
+
// Handle input change
|
|
89
|
+
const handleInputChange = React.useCallback((index, event) => {
|
|
98
90
|
const val = event.nativeEvent?.value || event.target?.value;
|
|
99
91
|
|
|
100
|
-
//
|
|
101
|
-
|
|
102
|
-
if (codeElement && codeElement.value) {
|
|
103
|
-
codeElement.value = numericOnly;
|
|
104
|
-
}
|
|
105
|
-
handleSingleCodes(codeId, numericOnly, numericOnly ? 'success' : '');
|
|
106
|
-
handleChangeCode();
|
|
107
|
-
if (nextIndex === validatorsLength) {
|
|
108
|
-
codeElement?.blur();
|
|
109
|
-
changeDataMasking(codeElement);
|
|
92
|
+
// This prevents the infinite loop where setting element.value triggers another input event
|
|
93
|
+
if (maskRef.current && val === maskRef.current.substring(0, 1)) {
|
|
110
94
|
return;
|
|
111
95
|
}
|
|
112
|
-
|
|
113
|
-
|
|
96
|
+
const numericOnly = val.replace(/\D/g, '').substring(0, 1);
|
|
97
|
+
|
|
98
|
+
// Update state
|
|
99
|
+
updateCode(index, numericOnly);
|
|
100
|
+
|
|
101
|
+
// Update DOM element
|
|
102
|
+
const element = codeReferences[index]?.current;
|
|
103
|
+
if (element) {
|
|
104
|
+
if (maskRef.current && numericOnly) {
|
|
105
|
+
element.value = maskRef.current.substring(0, 1);
|
|
106
|
+
} else {
|
|
107
|
+
element.value = numericOnly;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Move to next field if digit entered
|
|
112
|
+
if (numericOnly && index < validatorsLength - 1) {
|
|
113
|
+
const nextElement = codeReferences[index + 1]?.current;
|
|
114
114
|
nextElement?.focus();
|
|
115
|
-
|
|
115
|
+
} else if (index === validatorsLength - 1) {
|
|
116
|
+
element?.blur();
|
|
116
117
|
}
|
|
117
|
-
}, [codeReferences,
|
|
118
|
-
|
|
118
|
+
}, [codeReferences, updateCode, validatorsLength]);
|
|
119
|
+
|
|
120
|
+
// Handle backspace
|
|
121
|
+
const handleKeyPress = React.useCallback((index, event) => {
|
|
119
122
|
if (!(event.keyCode === 8 || event.code === 'Backspace')) {
|
|
120
123
|
return;
|
|
121
124
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
125
|
+
const currentElement = codeReferences[index]?.current;
|
|
126
|
+
if (currentElement) {
|
|
127
|
+
currentElement.value = '';
|
|
128
|
+
}
|
|
129
|
+
updateCode(index, '');
|
|
130
|
+
if (index > 0) {
|
|
131
|
+
const previousElement = codeReferences[index - 1]?.current;
|
|
128
132
|
previousElement?.focus();
|
|
129
133
|
}
|
|
130
|
-
|
|
131
|
-
handleChangeCode();
|
|
132
|
-
};
|
|
134
|
+
}, [codeReferences, updateCode]);
|
|
133
135
|
const getCodeComponents = () => {
|
|
134
136
|
return Array.from({
|
|
135
137
|
length: validatorsLength
|
|
@@ -138,11 +140,14 @@ const Validator = /*#__PURE__*/React.forwardRef((_ref2, ref) => {
|
|
|
138
140
|
const codeInputProps = {
|
|
139
141
|
nativeID: codeId,
|
|
140
142
|
keyboardType: 'numeric',
|
|
141
|
-
ref: codeReferences[
|
|
142
|
-
validation: strValidation ||
|
|
143
|
+
ref: codeReferences[i] ?? null,
|
|
144
|
+
validation: strValidation || (codes[i] ? 'success' : ''),
|
|
143
145
|
tokens: selectCodeTextInputTokens(themeTokens),
|
|
146
|
+
// Only use secureTextEntry in React Native, web handles mask differently
|
|
147
|
+
secureTextEntry: !!(Platform.OS !== 'web' && mask),
|
|
148
|
+
selectTextOnFocus: Platform.OS !== 'web',
|
|
144
149
|
onFocus: () => {
|
|
145
|
-
const element = codeReferences[
|
|
150
|
+
const element = codeReferences[i]?.current;
|
|
146
151
|
if (Platform.OS === 'web' && element?.select) {
|
|
147
152
|
return element.select() ?? null;
|
|
148
153
|
}
|
|
@@ -151,11 +156,35 @@ const Validator = /*#__PURE__*/React.forwardRef((_ref2, ref) => {
|
|
|
151
156
|
}
|
|
152
157
|
return null;
|
|
153
158
|
},
|
|
154
|
-
onKeyPress: event => handleKeyPress(
|
|
159
|
+
onKeyPress: event => handleKeyPress(i, event),
|
|
155
160
|
onMouseOver: handleMouseOver,
|
|
156
161
|
onMouseOut: handleMouseOut,
|
|
157
162
|
inactive
|
|
158
163
|
};
|
|
164
|
+
|
|
165
|
+
// For React Native, use onChangeText and maxLength
|
|
166
|
+
if (Platform.OS !== 'web') {
|
|
167
|
+
codeInputProps.maxLength = 1;
|
|
168
|
+
codeInputProps.value = codes[i];
|
|
169
|
+
codeInputProps.onChange = () => {};
|
|
170
|
+
codeInputProps.onChangeText = text => {
|
|
171
|
+
if (text) {
|
|
172
|
+
updateCode(i, text);
|
|
173
|
+
if (i < validatorsLength - 1) {
|
|
174
|
+
setTimeout(() => {
|
|
175
|
+
codeReferences[i + 1]?.current?.focus();
|
|
176
|
+
}, 50);
|
|
177
|
+
}
|
|
178
|
+
} else {
|
|
179
|
+
updateCode(i, '');
|
|
180
|
+
if (i > 0) {
|
|
181
|
+
setTimeout(() => {
|
|
182
|
+
codeReferences[i - 1]?.current?.focus();
|
|
183
|
+
}, 50);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
}
|
|
159
188
|
if (!codeInputProps.validation) {
|
|
160
189
|
delete codeInputProps.validation;
|
|
161
190
|
}
|
|
@@ -167,89 +196,96 @@ const Validator = /*#__PURE__*/React.forwardRef((_ref2, ref) => {
|
|
|
167
196
|
}, codeId);
|
|
168
197
|
});
|
|
169
198
|
};
|
|
199
|
+
|
|
200
|
+
// Sync external value prop to internal state
|
|
170
201
|
React.useEffect(() => {
|
|
171
|
-
if (Number(value).toString() !== 'NaN') {
|
|
172
|
-
|
|
202
|
+
if (value && Number(value).toString() !== 'NaN') {
|
|
203
|
+
const digits = value.split('').slice(0, validatorsLength);
|
|
204
|
+
const newCodes = Array(validatorsLength).fill('');
|
|
205
|
+
digits.forEach((digit, i) => {
|
|
206
|
+
if (/\d/.test(digit)) {
|
|
207
|
+
newCodes[i] = digit;
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
setCodes(newCodes);
|
|
173
211
|
}
|
|
174
|
-
}, [value]);
|
|
212
|
+
}, [value, validatorsLength]);
|
|
213
|
+
|
|
214
|
+
// Sync codes state to DOM elements
|
|
175
215
|
React.useEffect(() => {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
}, (_, i) => {
|
|
179
|
-
const element = codeReferences[prefix + i]?.current;
|
|
216
|
+
codes.forEach((code, i) => {
|
|
217
|
+
const element = codeReferences[i]?.current;
|
|
180
218
|
if (element && element.value !== undefined) {
|
|
181
|
-
if (mask &&
|
|
182
|
-
element.value = mask;
|
|
219
|
+
if (mask && code) {
|
|
220
|
+
element.value = mask.substring(0, 1);
|
|
183
221
|
} else {
|
|
184
|
-
element.value =
|
|
222
|
+
element.value = code;
|
|
185
223
|
}
|
|
186
224
|
}
|
|
187
|
-
handleSingleCodes(prefix + i, text[i] ?? '', text[i] ? 'success' : '');
|
|
188
|
-
return null;
|
|
189
225
|
});
|
|
190
|
-
}, [
|
|
226
|
+
}, [codes, codeReferences, mask]);
|
|
227
|
+
|
|
228
|
+
// Setup event listeners - only runs once on mount
|
|
191
229
|
React.useEffect(() => {
|
|
192
|
-
|
|
230
|
+
if (Platform.OS !== 'web') {
|
|
231
|
+
return undefined;
|
|
232
|
+
}
|
|
233
|
+
const handlePaste = event => {
|
|
193
234
|
event.preventDefault();
|
|
194
|
-
|
|
195
|
-
// Clear current state first
|
|
196
|
-
setText('');
|
|
197
|
-
|
|
198
|
-
// Clear all individual input fields and their state
|
|
199
|
-
Array.from({
|
|
200
|
-
length: validatorsLength
|
|
201
|
-
}, (_, i) => {
|
|
202
|
-
const element = codeReferences[prefix + i]?.current;
|
|
203
|
-
if (element && element.value !== undefined) {
|
|
204
|
-
element.value = '';
|
|
205
|
-
}
|
|
206
|
-
handleSingleCodes(prefix + i, '', '');
|
|
207
|
-
return null;
|
|
208
|
-
});
|
|
209
235
|
const clipBoardText = event.clipboardData.getData('text');
|
|
210
|
-
|
|
211
|
-
// Validate that input contains only digits and truncate to 6 characters
|
|
212
236
|
const numericOnly = clipBoardText.replace(/\D/g, '').substring(0, validatorsLength);
|
|
213
|
-
|
|
214
|
-
|
|
237
|
+
const newCodes = Array(validatorsLength).fill('');
|
|
238
|
+
numericOnly.split('').forEach((digit, i) => {
|
|
239
|
+
newCodes[i] = digit;
|
|
240
|
+
});
|
|
241
|
+
setCodes(newCodes);
|
|
242
|
+
if (onChangeRef.current) {
|
|
243
|
+
const singleCodesObj = {};
|
|
244
|
+
newCodes.forEach((code, i) => {
|
|
245
|
+
singleCodesObj[prefix + i] = code;
|
|
246
|
+
singleCodesObj[prefix + i + sufixValidation] = code ? 'success' : '';
|
|
247
|
+
});
|
|
248
|
+
onChangeRef.current(numericOnly, singleCodesObj);
|
|
215
249
|
}
|
|
216
250
|
};
|
|
217
251
|
const handleCopy = event => {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
252
|
+
setCodes(currentCodes => {
|
|
253
|
+
const clipBoardText = currentCodes.join('');
|
|
254
|
+
event.clipboardData.setData('text/plain', clipBoardText);
|
|
255
|
+
event.preventDefault();
|
|
256
|
+
return currentCodes;
|
|
257
|
+
});
|
|
223
258
|
};
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
259
|
+
|
|
260
|
+
// Add event listeners to each input
|
|
261
|
+
codeReferences.forEach((inputRef, i) => {
|
|
262
|
+
const element = inputRef?.current;
|
|
263
|
+
if (element && typeof element.addEventListener === 'function') {
|
|
264
|
+
element.addEventListener('paste', handlePaste);
|
|
265
|
+
element.addEventListener('copy', handleCopy);
|
|
266
|
+
element.addEventListener('input', event => handleInputChange(i, event));
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// Cleanup
|
|
271
|
+
return () => {
|
|
272
|
+
codeReferences.forEach(inputRef => {
|
|
273
|
+
const element = inputRef?.current;
|
|
274
|
+
if (element && typeof element.removeEventListener === 'function') {
|
|
275
|
+
element.removeEventListener('paste', handlePaste);
|
|
276
|
+
element.removeEventListener('copy', handleCopy);
|
|
277
|
+
element.removeEventListener('input', event => handleInputChange(event.target.dataset.index, event));
|
|
233
278
|
}
|
|
234
|
-
return null;
|
|
235
279
|
});
|
|
236
|
-
}
|
|
237
|
-
return () => {
|
|
238
|
-
if (Platform.OS === 'web') {
|
|
239
|
-
Array.from({
|
|
240
|
-
length: validatorsLength
|
|
241
|
-
}, (_, i) => {
|
|
242
|
-
const element = codeReferences[prefix + i]?.current;
|
|
243
|
-
if (element && typeof element.removeEventListener === 'function') {
|
|
244
|
-
element.removeEventListener('paste', handlePasteCode);
|
|
245
|
-
element.removeEventListener('copy', handleCopy);
|
|
246
|
-
element.removeEventListener('input', event => handleChangeCodeValues(event, prefix + i, i + 1));
|
|
247
|
-
}
|
|
248
|
-
return null;
|
|
249
|
-
});
|
|
250
|
-
}
|
|
251
280
|
};
|
|
252
|
-
|
|
281
|
+
/*
|
|
282
|
+
* codeReferences and handleInputChange are intentionally omitted from dependencies
|
|
283
|
+
* because we want event listeners to be registered ONLY ONCE when the component mounts.
|
|
284
|
+
* codeReferences is stable (created with useMemo) and handleInputChange is stable (useCallback).
|
|
285
|
+
* Including them would cause unnecessary re-registration of listeners on each render.
|
|
286
|
+
*/
|
|
287
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
288
|
+
}, []);
|
|
253
289
|
return /*#__PURE__*/_jsx(InputSupports, {
|
|
254
290
|
...supportsProps,
|
|
255
291
|
feedbackProps: {
|
package/lib/esm/index.js
CHANGED
|
@@ -68,6 +68,6 @@ export { default as BaseProvider } from './BaseProvider';
|
|
|
68
68
|
export { useHydrationContext } from './BaseProvider/HydrationContext';
|
|
69
69
|
export { default as Validator } from './Validator';
|
|
70
70
|
export { default as ViewportProvider, useViewport, ViewportContext } from './ViewportProvider';
|
|
71
|
-
export { default as ThemeProvider, useTheme, useSetTheme, useThemeTokens, useThemeTokensCallback, getThemeTokens, applyOuterBorder, applyTextStyles, applyShadowToken, useResponsiveThemeTokens } from './ThemeProvider';
|
|
71
|
+
export { default as ThemeProvider, useTheme, useSetTheme, useThemeTokens, useThemeTokensCallback, getThemeTokens, applyOuterBorder, applyTextStyles, applyShadowToken, useResponsiveThemeTokens, useResponsiveThemeTokensCallback } from './ThemeProvider';
|
|
72
72
|
export * from './utils';
|
|
73
73
|
export { default as Portal } from './Portal';
|
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.
|
|
15
|
+
"@telus-uds/system-theme-tokens": "^4.16.0",
|
|
16
16
|
"airbnb-prop-types": "^2.16.0",
|
|
17
17
|
"css-mediaquery": "^0.1.2",
|
|
18
18
|
"expo-document-picker": "^13.0.1",
|
|
@@ -84,6 +84,6 @@
|
|
|
84
84
|
"standard-engine": {
|
|
85
85
|
"skip": true
|
|
86
86
|
},
|
|
87
|
-
"version": "3.
|
|
87
|
+
"version": "3.23.0",
|
|
88
88
|
"types": "types/index.d.ts"
|
|
89
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.
|
|
15
|
+
"@telus-uds/system-theme-tokens": "^4.16.0",
|
|
16
16
|
"airbnb-prop-types": "^2.16.0",
|
|
17
17
|
"css-mediaquery": "^0.1.2",
|
|
18
18
|
"expo-document-picker": "^13.0.1",
|
|
@@ -84,6 +84,6 @@
|
|
|
84
84
|
"standard-engine": {
|
|
85
85
|
"skip": true
|
|
86
86
|
},
|
|
87
|
-
"version": "3.
|
|
87
|
+
"version": "3.23.0",
|
|
88
88
|
"types": "types/index.d.ts"
|
|
89
89
|
}
|
package/src/Button/Button.jsx
CHANGED
|
@@ -2,17 +2,38 @@ import React from 'react'
|
|
|
2
2
|
|
|
3
3
|
import ButtonBase from './ButtonBase'
|
|
4
4
|
import buttonPropTypes, { textAndA11yText } from './propTypes'
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
useThemeTokensCallback,
|
|
7
|
+
useResponsiveThemeTokensCallback,
|
|
8
|
+
useTheme
|
|
9
|
+
} from '../ThemeProvider'
|
|
6
10
|
import { a11yProps } from '../utils/props'
|
|
7
11
|
import { useViewport } from '../ViewportProvider'
|
|
8
12
|
|
|
9
13
|
const Button = React.forwardRef(
|
|
10
|
-
({ accessibilityRole = 'button', tokens, variant, ...props }, ref) => {
|
|
14
|
+
({ accessibilityRole = 'button', tokens, variant, heightFull = true, ...props }, ref) => {
|
|
11
15
|
const viewport = useViewport()
|
|
12
|
-
const
|
|
13
|
-
|
|
16
|
+
const {
|
|
17
|
+
themeOptions: { enableMediaQueryStyleSheet }
|
|
18
|
+
} = useTheme()
|
|
19
|
+
|
|
20
|
+
const buttonVariant = enableMediaQueryStyleSheet ? variant : { viewport, ...variant }
|
|
21
|
+
|
|
22
|
+
const useTokens = enableMediaQueryStyleSheet
|
|
23
|
+
? useResponsiveThemeTokensCallback
|
|
24
|
+
: useThemeTokensCallback
|
|
25
|
+
|
|
26
|
+
const getTokens = useTokens('Button', tokens, buttonVariant)
|
|
27
|
+
|
|
14
28
|
return (
|
|
15
|
-
<ButtonBase
|
|
29
|
+
<ButtonBase
|
|
30
|
+
{...props}
|
|
31
|
+
tokens={getTokens}
|
|
32
|
+
heightFull={heightFull}
|
|
33
|
+
accessibilityRole={accessibilityRole}
|
|
34
|
+
ref={ref}
|
|
35
|
+
viewport={viewport}
|
|
36
|
+
/>
|
|
16
37
|
)
|
|
17
38
|
}
|
|
18
39
|
)
|