@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.
- package/CHANGELOG.md +34 -2
- package/lib/A11yInfoProvider/index.js +2 -2
- package/lib/Autocomplete/Autocomplete.js +22 -32
- package/lib/Autocomplete/Suggestions.js +1 -1
- package/lib/BaseProvider/HydrationContext.js +1 -2
- package/lib/BaseProvider/index.js +1 -2
- package/lib/Button/ButtonDropdown.js +1 -1
- package/lib/Card/Card.js +12 -13
- package/lib/Card/CardBase.js +1 -1
- package/lib/Card/PressableCardBase.js +1 -1
- package/lib/CardGroup/CardGroup.js +3 -3
- package/lib/Carousel/Carousel.js +5 -6
- package/lib/Carousel/CarouselStepTracker/CarouselStepTracker.js +3 -0
- package/lib/Carousel/CarouselTabs/CarouselTabs.js +1 -2
- package/lib/Carousel/CarouselTabs/CarouselTabsPanel.js +1 -1
- package/lib/Carousel/CarouselTabs/CarouselTabsPanelItem.js +1 -1
- package/lib/Carousel/CarouselThumbnail.js +1 -1
- package/lib/Checkbox/Checkbox.js +1 -1
- package/lib/ColourToggle/ColourToggle.js +1 -1
- package/lib/ExpandCollapseMini/ExpandCollapseMini.js +77 -0
- package/lib/ExpandCollapseMini/ExpandCollapseMiniControl.js +126 -0
- package/lib/ExpandCollapseMini/index.js +2 -0
- package/lib/Footnote/Footnote.js +4 -4
- package/lib/HorizontalScroll/HorizontalScroll.js +1 -2
- package/lib/Icon/Icon.js +1 -1
- package/lib/Icon/IconText.js +2 -3
- package/lib/IconButton/IconButton.js +1 -2
- package/lib/InputLabel/InputLabel.js +36 -2
- package/lib/InputSupports/InputSupports.js +31 -8
- package/lib/InputSupports/dictionary.js +12 -0
- package/lib/InputSupports/useInputSupports.js +12 -3
- package/lib/Link/LinkBase.js +25 -18
- package/lib/List/List.js +1 -2
- package/lib/List/ListItemContent.js +1 -1
- package/lib/Listbox/Listbox.js +5 -8
- package/lib/Listbox/PressableItem.js +4 -4
- package/lib/Modal/Modal.js +4 -7
- package/lib/MultiSelectFilter/MultiSelectFilter.js +55 -42
- package/lib/Notification/Notification.js +15 -13
- package/lib/OrderedList/OrderedList.js +2 -3
- package/lib/Pagination/usePagination.js +1 -2
- package/lib/PriceLockup/utils/renderFootnoteContent.js +2 -2
- package/lib/PriceLockup/utils/renderFootnoteLinks.js +2 -2
- package/lib/PriceLockup/utils/renderPrice.js +2 -2
- package/lib/ProductCard/ProductCard.js +2 -3
- package/lib/Progress/ProgressBarBackground.js +2 -2
- package/lib/QuickLinksFeature/QuickLinksFeature.js +1 -2
- package/lib/Radio/Radio.js +1 -1
- package/lib/Search/Search.js +41 -11
- package/lib/Select/Picker.js +2 -2
- package/lib/Select/Picker.native.js +8 -4
- package/lib/Select/constants.js +4 -2
- package/lib/StackView/StackWrap.js +1 -4
- package/lib/StackView/getStackedContent.js +1 -2
- package/lib/StepTracker/StepTracker.js +1 -2
- package/lib/TabBar/TabBar.js +1 -1
- package/lib/Tabs/Tabs.js +1 -1
- package/lib/Tabs/TabsItem.js +2 -2
- package/lib/TextInput/TextArea.js +7 -6
- package/lib/TextInput/TextInput.js +7 -6
- package/lib/TextInput/TextInputBase.js +57 -25
- package/lib/ThemeProvider/utils/theme-tokens.js +2 -4
- package/lib/Timeline/Timeline.js +1 -2
- package/lib/Tooltip/Tooltip.native.js +4 -4
- package/lib/Typography/Typography.js +4 -5
- package/lib/Validator/Validator.js +9 -14
- package/lib/ViewportProvider/useViewportListener.js +1 -1
- package/lib/index.js +1 -0
- package/lib/utils/children.js +2 -6
- package/lib/utils/input.js +1 -1
- package/lib/utils/props/componentPropType.js +1 -2
- package/lib/utils/props/inputSupportsProps.js +15 -3
- package/lib/utils/props/selectSystemProps.js +2 -2
- package/lib/utils/ssr-media-query/create-stylesheet/create-stylesheet-mobile.js +1 -1
- package/lib/utils/ssr-media-query/create-stylesheet/index.js +2 -3
- package/lib/utils/ssr-media-query/utils/inject.js +3 -5
- package/lib/utils/useHash.js +1 -4
- package/lib/utils/useOverlaidPosition.js +25 -4
- package/lib/utils/useScrollBlocking.js +2 -4
- package/lib/utils/useSpacingScale.js +2 -2
- package/package.json +2 -2
- package/src/Carousel/CarouselStepTracker/CarouselStepTracker.jsx +3 -0
- package/src/ExpandCollapseMini/ExpandCollapseMini.jsx +76 -0
- package/src/ExpandCollapseMini/ExpandCollapseMiniControl.jsx +119 -0
- package/src/ExpandCollapseMini/index.js +3 -0
- package/src/InputLabel/InputLabel.jsx +39 -2
- package/src/InputSupports/InputSupports.jsx +33 -7
- package/src/InputSupports/dictionary.js +12 -0
- package/src/InputSupports/useInputSupports.js +24 -3
- package/src/Link/LinkBase.jsx +25 -18
- package/src/Modal/Modal.jsx +1 -1
- package/src/MultiSelectFilter/MultiSelectFilter.jsx +55 -27
- package/src/Notification/Notification.jsx +9 -3
- package/src/Search/Search.jsx +52 -24
- package/src/Select/Picker.native.jsx +10 -4
- package/src/Select/constants.js +4 -1
- package/src/TextInput/TextArea.jsx +12 -5
- package/src/TextInput/TextInput.jsx +13 -6
- package/src/TextInput/TextInputBase.jsx +57 -10
- package/src/index.js +1 -0
- package/src/utils/props/inputSupportsProps.js +15 -3
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (!
|
|
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 && (
|
|
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.
|
|
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.
|
|
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
|
|
@@ -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'
|
|
52
|
-
|
|
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={
|
|
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 = ({
|
|
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([
|
|
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
|
}
|
package/src/Link/LinkBase.jsx
CHANGED
|
@@ -32,21 +32,15 @@ const selectOuterBorderStyles = ({
|
|
|
32
32
|
outerBorderGap,
|
|
33
33
|
borderRadius,
|
|
34
34
|
outerBorderOutline
|
|
35
|
-
}) =>
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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 =
|
|
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
|
|
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
|
|
package/src/Modal/Modal.jsx
CHANGED