@telus-uds/components-base 3.22.0 → 3.24.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 +29 -1
- package/lib/cjs/Button/Button.js +2 -0
- package/lib/cjs/Button/ButtonBase.js +10 -5
- 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/CardBase.js +97 -17
- package/lib/cjs/Card/PressableCardBase.js +12 -8
- package/lib/cjs/Carousel/Carousel.js +52 -19
- package/lib/cjs/Carousel/CarouselItem/CarouselItem.js +23 -3
- package/lib/cjs/HorizontalScroll/HorizontalScroll.js +5 -2
- package/lib/cjs/Icon/Icon.js +11 -11
- package/lib/cjs/Icon/IconText.js +0 -1
- package/lib/cjs/Listbox/GroupControl.js +44 -44
- package/lib/cjs/Listbox/Listbox.js +63 -20
- package/lib/cjs/Listbox/ListboxGroup.js +141 -9
- package/lib/cjs/Listbox/ListboxOverlay.js +13 -5
- package/lib/cjs/Listbox/PressableItem.js +8 -4
- package/lib/cjs/Listbox/SecondLevelHeader.js +201 -0
- package/lib/cjs/Listbox/dictionary.js +14 -0
- package/lib/cjs/Shortcuts/Shortcuts.js +169 -0
- package/lib/cjs/Shortcuts/ShortcutsItem.js +280 -0
- package/lib/cjs/Shortcuts/index.js +16 -0
- package/lib/cjs/TextInput/TextInputBase.js +5 -1
- package/lib/cjs/Tooltip/Tooltip.native.js +2 -0
- package/lib/cjs/Validator/Validator.js +171 -135
- package/lib/cjs/index.js +15 -0
- package/lib/esm/Button/Button.js +2 -0
- package/lib/esm/Button/ButtonBase.js +10 -5
- 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/CardBase.js +97 -17
- package/lib/esm/Card/PressableCardBase.js +10 -8
- package/lib/esm/Carousel/Carousel.js +52 -19
- package/lib/esm/Carousel/CarouselItem/CarouselItem.js +23 -3
- package/lib/esm/HorizontalScroll/HorizontalScroll.js +6 -3
- package/lib/esm/Icon/Icon.js +11 -11
- package/lib/esm/Icon/IconText.js +0 -1
- package/lib/esm/Listbox/GroupControl.js +44 -44
- package/lib/esm/Listbox/Listbox.js +64 -21
- package/lib/esm/Listbox/ListboxGroup.js +143 -11
- package/lib/esm/Listbox/ListboxOverlay.js +13 -5
- package/lib/esm/Listbox/PressableItem.js +8 -4
- package/lib/esm/Listbox/SecondLevelHeader.js +194 -0
- package/lib/esm/Listbox/dictionary.js +8 -0
- package/lib/esm/Shortcuts/Shortcuts.js +160 -0
- package/lib/esm/Shortcuts/ShortcutsItem.js +273 -0
- package/lib/esm/Shortcuts/index.js +3 -0
- package/lib/esm/TextInput/TextInputBase.js +5 -1
- package/lib/esm/Tooltip/Tooltip.native.js +2 -0
- package/lib/esm/Validator/Validator.js +171 -135
- package/lib/esm/index.js +1 -0
- package/lib/package.json +2 -2
- package/package.json +2 -2
- package/src/Button/Button.jsx +2 -1
- package/src/Button/ButtonBase.jsx +18 -12
- package/src/Button/ButtonDropdown.jsx +2 -0
- package/src/Button/ButtonGroup.jsx +62 -45
- package/src/Button/propTypes.js +6 -0
- package/src/Card/CardBase.jsx +113 -14
- package/src/Card/PressableCardBase.jsx +17 -5
- package/src/Carousel/Carousel.jsx +58 -5
- package/src/Carousel/CarouselItem/CarouselItem.jsx +31 -3
- package/src/HorizontalScroll/HorizontalScroll.jsx +6 -3
- package/src/Icon/Icon.jsx +14 -14
- package/src/Icon/IconText.jsx +0 -1
- package/src/Listbox/GroupControl.jsx +72 -70
- package/src/Listbox/Listbox.jsx +67 -11
- package/src/Listbox/ListboxGroup.jsx +160 -27
- package/src/Listbox/ListboxOverlay.jsx +23 -5
- package/src/Listbox/PressableItem.jsx +8 -4
- package/src/Listbox/SecondLevelHeader.jsx +182 -0
- package/src/Listbox/dictionary.js +8 -0
- package/src/Shortcuts/Shortcuts.jsx +174 -0
- package/src/Shortcuts/ShortcutsItem.jsx +297 -0
- package/src/Shortcuts/index.js +4 -0
- package/src/TextInput/TextInputBase.jsx +5 -1
- package/src/Tooltip/Tooltip.native.jsx +2 -1
- package/src/Validator/Validator.jsx +180 -159
- package/src/index.js +1 -0
- package/types/Listbox.d.ts +24 -0
- package/types/Shortcuts.d.ts +136 -0
- package/types/index.d.ts +12 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import PropTypes from 'prop-types'
|
|
3
|
+
import { View, StyleSheet, Text, Pressable } from 'react-native'
|
|
4
|
+
import { useThemeTokens } from '../ThemeProvider'
|
|
5
|
+
import { useCopy, variantProp, copyPropTypes } from '../utils'
|
|
6
|
+
import Icon from '../Icon'
|
|
7
|
+
import IconButton from '../IconButton'
|
|
8
|
+
import Divider from '../Divider'
|
|
9
|
+
import defaultDictionary from './dictionary'
|
|
10
|
+
|
|
11
|
+
const styles = StyleSheet.create({
|
|
12
|
+
headerContainer: {
|
|
13
|
+
width: '100%'
|
|
14
|
+
},
|
|
15
|
+
headerContent: {
|
|
16
|
+
flexDirection: 'row',
|
|
17
|
+
alignItems: 'center',
|
|
18
|
+
width: '100%'
|
|
19
|
+
},
|
|
20
|
+
leftSection: {
|
|
21
|
+
flexDirection: 'row',
|
|
22
|
+
alignItems: 'center',
|
|
23
|
+
flex: 1
|
|
24
|
+
},
|
|
25
|
+
backIcon: {
|
|
26
|
+
marginRight: 8,
|
|
27
|
+
flexShrink: 0
|
|
28
|
+
},
|
|
29
|
+
labelText: {
|
|
30
|
+
flex: 1
|
|
31
|
+
},
|
|
32
|
+
closeButton: {
|
|
33
|
+
flexShrink: 0
|
|
34
|
+
},
|
|
35
|
+
dividerContainer: {
|
|
36
|
+
width: '100%'
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
const selectHeaderContainerStyles = ({ secondLevelHeaderBackgroundColor }) => ({
|
|
41
|
+
backgroundColor: secondLevelHeaderBackgroundColor
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
const selectHeaderContentStyles = ({
|
|
45
|
+
secondLevelHeaderPaddingTop,
|
|
46
|
+
secondLevelHeaderPaddingBottom,
|
|
47
|
+
secondLevelHeaderPaddingLeft,
|
|
48
|
+
secondLevelHeaderPaddingRight
|
|
49
|
+
}) => ({
|
|
50
|
+
paddingTop: secondLevelHeaderPaddingTop,
|
|
51
|
+
paddingBottom: secondLevelHeaderPaddingBottom,
|
|
52
|
+
paddingLeft: secondLevelHeaderPaddingLeft,
|
|
53
|
+
paddingRight: secondLevelHeaderPaddingRight
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
const selectLabelTextStyles = ({
|
|
57
|
+
secondLevelBackLinkFontName,
|
|
58
|
+
secondLevelBackLinkFontWeight,
|
|
59
|
+
secondLevelBackLinkFontSize,
|
|
60
|
+
secondLevelBackLinkColor
|
|
61
|
+
}) => ({
|
|
62
|
+
fontFamily: `${secondLevelBackLinkFontName}${secondLevelBackLinkFontWeight}normal`,
|
|
63
|
+
fontSize: secondLevelBackLinkFontSize,
|
|
64
|
+
color: secondLevelBackLinkColor
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* SecondLevelHeader component for Listbox secondLevel variant.
|
|
69
|
+
* Displays a header with back button icon, title text, and close button (IconButton),
|
|
70
|
+
* separated from content by a Divider.
|
|
71
|
+
*/
|
|
72
|
+
const SecondLevelHeader = React.forwardRef(
|
|
73
|
+
(
|
|
74
|
+
{
|
|
75
|
+
label,
|
|
76
|
+
onBack,
|
|
77
|
+
onClose,
|
|
78
|
+
copy = 'en',
|
|
79
|
+
dictionary = defaultDictionary,
|
|
80
|
+
tokens: tokensProp = {},
|
|
81
|
+
variant = {}
|
|
82
|
+
},
|
|
83
|
+
ref
|
|
84
|
+
) => {
|
|
85
|
+
const tokens = useThemeTokens('Listbox', variant, tokensProp)
|
|
86
|
+
const getCopy = useCopy({ dictionary, copy })
|
|
87
|
+
|
|
88
|
+
const {
|
|
89
|
+
secondLevelBackIcon,
|
|
90
|
+
secondLevelBackIconColor,
|
|
91
|
+
secondLevelCloseIcon,
|
|
92
|
+
secondLevelCloseIconSize,
|
|
93
|
+
secondLevelCloseButtonBorderWidth,
|
|
94
|
+
secondLevelCloseButtonPadding,
|
|
95
|
+
secondLevelDividerColor,
|
|
96
|
+
secondLevelDividerWidth
|
|
97
|
+
} = tokens
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<View style={[styles.headerContainer, selectHeaderContainerStyles(tokens)]} ref={ref}>
|
|
101
|
+
<View style={[styles.headerContent, selectHeaderContentStyles(tokens)]}>
|
|
102
|
+
<Pressable onPress={onBack} style={styles.leftSection}>
|
|
103
|
+
<View style={styles.backIcon}>
|
|
104
|
+
<Icon
|
|
105
|
+
icon={secondLevelBackIcon}
|
|
106
|
+
tokens={{
|
|
107
|
+
color: secondLevelBackIconColor
|
|
108
|
+
}}
|
|
109
|
+
variant={{ size: 'micro' }}
|
|
110
|
+
/>
|
|
111
|
+
</View>
|
|
112
|
+
<Text numberOfLines={1} style={[styles.labelText, selectLabelTextStyles(tokens)]}>
|
|
113
|
+
{label}
|
|
114
|
+
</Text>
|
|
115
|
+
</Pressable>
|
|
116
|
+
<View style={styles.closeButton}>
|
|
117
|
+
<IconButton
|
|
118
|
+
icon={secondLevelCloseIcon}
|
|
119
|
+
onPress={onClose}
|
|
120
|
+
accessibilityLabel={getCopy('closeMenu')}
|
|
121
|
+
tokens={{
|
|
122
|
+
iconSize: secondLevelCloseIconSize,
|
|
123
|
+
borderWidth: secondLevelCloseButtonBorderWidth,
|
|
124
|
+
padding: secondLevelCloseButtonPadding
|
|
125
|
+
}}
|
|
126
|
+
/>
|
|
127
|
+
</View>
|
|
128
|
+
</View>
|
|
129
|
+
<View style={styles.dividerContainer}>
|
|
130
|
+
<Divider
|
|
131
|
+
tokens={{
|
|
132
|
+
color: secondLevelDividerColor,
|
|
133
|
+
width: secondLevelDividerWidth
|
|
134
|
+
}}
|
|
135
|
+
/>
|
|
136
|
+
</View>
|
|
137
|
+
</View>
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
SecondLevelHeader.displayName = 'SecondLevelHeader'
|
|
143
|
+
|
|
144
|
+
SecondLevelHeader.propTypes = {
|
|
145
|
+
/**
|
|
146
|
+
* The label text to display (typically the parent item label)
|
|
147
|
+
*/
|
|
148
|
+
label: PropTypes.string.isRequired,
|
|
149
|
+
/**
|
|
150
|
+
* Callback when back button is clicked
|
|
151
|
+
*/
|
|
152
|
+
onBack: PropTypes.func.isRequired,
|
|
153
|
+
/**
|
|
154
|
+
* Callback when close button is clicked
|
|
155
|
+
*/
|
|
156
|
+
onClose: PropTypes.func.isRequired,
|
|
157
|
+
/**
|
|
158
|
+
* Select English or French copy
|
|
159
|
+
*/
|
|
160
|
+
copy: copyPropTypes,
|
|
161
|
+
/**
|
|
162
|
+
* Override the default dictionary, by passing the complete dictionary object for `en` and `fr`
|
|
163
|
+
*/
|
|
164
|
+
dictionary: PropTypes.shape({
|
|
165
|
+
en: PropTypes.shape({
|
|
166
|
+
closeMenu: PropTypes.string.isRequired
|
|
167
|
+
}),
|
|
168
|
+
fr: PropTypes.shape({
|
|
169
|
+
closeMenu: PropTypes.string.isRequired
|
|
170
|
+
})
|
|
171
|
+
}),
|
|
172
|
+
/**
|
|
173
|
+
* Custom tokens to override theme tokens
|
|
174
|
+
*/
|
|
175
|
+
tokens: PropTypes.object,
|
|
176
|
+
/**
|
|
177
|
+
* Variant configuration
|
|
178
|
+
*/
|
|
179
|
+
variant: variantProp.propType
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export default SecondLevelHeader
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import PropTypes from 'prop-types'
|
|
3
|
+
import { Platform, StyleSheet, View } from 'react-native'
|
|
4
|
+
import { viewports } from '@telus-uds/system-constants'
|
|
5
|
+
|
|
6
|
+
import { useTheme, useThemeTokens } from '../ThemeProvider'
|
|
7
|
+
import { useViewport } from '../ViewportProvider'
|
|
8
|
+
import {
|
|
9
|
+
a11yProps,
|
|
10
|
+
getTokensPropType,
|
|
11
|
+
selectSystemProps,
|
|
12
|
+
useResponsiveProp,
|
|
13
|
+
variantProp,
|
|
14
|
+
viewProps
|
|
15
|
+
} from '../utils'
|
|
16
|
+
|
|
17
|
+
import HorizontalScroll, {
|
|
18
|
+
horizontalScrollUtils,
|
|
19
|
+
HorizontalScrollButton
|
|
20
|
+
} from '../HorizontalScroll'
|
|
21
|
+
|
|
22
|
+
const [selectProps, selectedSystemPropTypes] = selectSystemProps([a11yProps, viewProps])
|
|
23
|
+
|
|
24
|
+
const { selectHorizontalScrollTokens, useItemPositions } = horizontalScrollUtils
|
|
25
|
+
|
|
26
|
+
const selectStyles = (themeTokens, maxWidth, viewport) => {
|
|
27
|
+
const isDesktop =
|
|
28
|
+
viewport === viewports.md || viewport === viewports.lg || viewport === viewports.xl
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
wrapper: {
|
|
32
|
+
alignItems: isDesktop ? 'center' : 'flex-start'
|
|
33
|
+
},
|
|
34
|
+
scrollContainer: {
|
|
35
|
+
width: '100%',
|
|
36
|
+
...(isDesktop && { maxWidth })
|
|
37
|
+
},
|
|
38
|
+
container: {
|
|
39
|
+
paddingTop: themeTokens.mainContainerTopPadding,
|
|
40
|
+
paddingBottom: themeTokens.mainContainerBottomPadding,
|
|
41
|
+
paddingLeft: themeTokens.mainContainerLeftPadding,
|
|
42
|
+
paddingRight: themeTokens.mainContainerRightPadding,
|
|
43
|
+
gap: themeTokens.mainContainerGap,
|
|
44
|
+
...(isDesktop && {
|
|
45
|
+
alignItems: 'flex-start',
|
|
46
|
+
justifyContent: 'center'
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* A horizontal scrollable shortcuts component that displays a collection of shortcut items.
|
|
54
|
+
* This component automatically injects shared configuration props to all ShortcutsItem children
|
|
55
|
+
* via React.cloneElement, including variant settings, hideLabels, and iconVariant.
|
|
56
|
+
*
|
|
57
|
+
* @component
|
|
58
|
+
* @param {Object} props - Component properties
|
|
59
|
+
* @param {Object} [props.tokens] - Theme tokens to customize the component's appearance
|
|
60
|
+
* @param {Object} [props.variant] - Visual variant configuration for the shortcuts container and its items
|
|
61
|
+
* @param {string} [props.variant.width] - Width variant to apply to all items (e.g., 'equal', 'dynamic')
|
|
62
|
+
* @param {Object} [props.scrollButtonTokens] - Tokens to customize scroll button appearance
|
|
63
|
+
* @param {boolean} [props.hideLabels=false] - Whether to hide labels on all shortcut items (can be overridden per item)
|
|
64
|
+
* @param {Object} [props.iconVariant] - Icon variant to apply to all shortcut items (can be overridden per item)
|
|
65
|
+
* @param {React.ReactNode} props.children - ShortcutsItem components to render
|
|
66
|
+
* @param {React.Ref} ref - Forwarded ref to the component's root element
|
|
67
|
+
* @returns {React.ReactElement} Rendered shortcuts component with horizontal scroll functionality
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* <Shortcuts hideLabels={false} variant={{ width: 'equal' }}>
|
|
71
|
+
* <ShortcutsItem icon={HomeIcon} label="Home" href="/home" />
|
|
72
|
+
* <ShortcutsItem icon={SettingsIcon} label="Settings" href="/settings" />
|
|
73
|
+
* </Shortcuts>
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* // Item-level props override container props
|
|
77
|
+
* <Shortcuts hideLabels iconVariant={{ size: 'small' }}>
|
|
78
|
+
* <ShortcutsItem icon={HomeIcon} label="Home" hideLabel={false} />
|
|
79
|
+
* <ShortcutsItem icon={SettingsIcon} label="Settings" />
|
|
80
|
+
* </Shortcuts>
|
|
81
|
+
*/
|
|
82
|
+
const Shortcuts = React.forwardRef(
|
|
83
|
+
(
|
|
84
|
+
{ tokens, variant, scrollButtonTokens, hideLabels = false, iconVariant, children, ...rest },
|
|
85
|
+
ref
|
|
86
|
+
) => {
|
|
87
|
+
const viewport = useViewport()
|
|
88
|
+
const themeTokens = useThemeTokens('Shortcuts', tokens, variant, {
|
|
89
|
+
viewport
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
const { themeOptions } = useTheme()
|
|
93
|
+
const maxWidth = useResponsiveProp(
|
|
94
|
+
themeOptions?.contentMaxWidth,
|
|
95
|
+
viewports.map.get(viewports.xl)
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
const [itemPositions] = useItemPositions()
|
|
99
|
+
|
|
100
|
+
const [maxItemWidth, setMaxItemWidth] = React.useState(null)
|
|
101
|
+
|
|
102
|
+
const registerWidth = React.useCallback(
|
|
103
|
+
(width) => setMaxItemWidth((prev) => (prev == null || width > prev ? width : prev)),
|
|
104
|
+
[]
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
const styles = selectStyles(themeTokens, maxWidth, viewport)
|
|
108
|
+
|
|
109
|
+
const childrenWithProps = React.Children.map(children, (child) => {
|
|
110
|
+
if (!React.isValidElement(child)) {
|
|
111
|
+
return child
|
|
112
|
+
}
|
|
113
|
+
return React.cloneElement(child, {
|
|
114
|
+
maxWidth: maxItemWidth,
|
|
115
|
+
registerWidth,
|
|
116
|
+
containerVariant: variant,
|
|
117
|
+
containerHideLabels: hideLabels,
|
|
118
|
+
containerIconVariant: iconVariant
|
|
119
|
+
})
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<View style={[staticStyles.wrapper, styles.wrapper]} ref={ref} {...selectProps(rest)}>
|
|
124
|
+
<View style={styles.scrollContainer}>
|
|
125
|
+
<HorizontalScroll
|
|
126
|
+
ScrollButton={HorizontalScrollButton}
|
|
127
|
+
itemPositions={itemPositions}
|
|
128
|
+
tokens={selectHorizontalScrollTokens(themeTokens)}
|
|
129
|
+
scrollButtonTokens={scrollButtonTokens}
|
|
130
|
+
variant={{ hideNavigationButtons: Platform.OS !== 'web' }}
|
|
131
|
+
>
|
|
132
|
+
<View style={[staticStyles.container, styles.container]}>{childrenWithProps}</View>
|
|
133
|
+
</HorizontalScroll>
|
|
134
|
+
</View>
|
|
135
|
+
</View>
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
Shortcuts.displayName = 'Shortcuts'
|
|
141
|
+
|
|
142
|
+
Shortcuts.propTypes = {
|
|
143
|
+
...selectedSystemPropTypes,
|
|
144
|
+
tokens: getTokensPropType('Shortcuts'),
|
|
145
|
+
variant: variantProp.propType,
|
|
146
|
+
/**
|
|
147
|
+
* Custom tokens for `HorizontalScrollButton`
|
|
148
|
+
*/
|
|
149
|
+
scrollButtonTokens: getTokensPropType('HorizontalScrollButton'),
|
|
150
|
+
/**
|
|
151
|
+
* Hide labels for all ShortcutsItem children. When true, labels are visually hidden but remain accessible to screen readers via the icon's accessibilityLabel.
|
|
152
|
+
*/
|
|
153
|
+
hideLabels: PropTypes.bool,
|
|
154
|
+
/**
|
|
155
|
+
* Icon variant to apply to all ShortcutsItem children.
|
|
156
|
+
*/
|
|
157
|
+
iconVariant: variantProp.propType,
|
|
158
|
+
/**
|
|
159
|
+
* ShortcutsItem components to be rendered within the Shortcuts container
|
|
160
|
+
*/
|
|
161
|
+
children: PropTypes.node
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const staticStyles = StyleSheet.create({
|
|
165
|
+
wrapper: {
|
|
166
|
+
flexGrow: 1
|
|
167
|
+
},
|
|
168
|
+
container: {
|
|
169
|
+
flexDirection: 'row',
|
|
170
|
+
flex: 1
|
|
171
|
+
}
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
export default Shortcuts
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { Image, Platform, Pressable, StyleSheet, View } from 'react-native'
|
|
3
|
+
import PropTypes from 'prop-types'
|
|
4
|
+
|
|
5
|
+
import { applyTextStyles, useThemeTokensCallback } from '../ThemeProvider'
|
|
6
|
+
import {
|
|
7
|
+
a11yProps,
|
|
8
|
+
clickProps,
|
|
9
|
+
getTokensPropType,
|
|
10
|
+
hrefAttrsProp,
|
|
11
|
+
linkProps,
|
|
12
|
+
resolvePressableState,
|
|
13
|
+
selectSystemProps,
|
|
14
|
+
variantProp,
|
|
15
|
+
viewProps,
|
|
16
|
+
wrapStringsInText
|
|
17
|
+
} from '../utils'
|
|
18
|
+
import Icon from '../Icon'
|
|
19
|
+
|
|
20
|
+
const DYNAMIC_WIDTH_VARIANT = 'dynamic'
|
|
21
|
+
const EQUAL_WIDTH_VARIANT = 'equal'
|
|
22
|
+
|
|
23
|
+
const [selectProps, selectedSystemPropTypes] = selectSystemProps([a11yProps, linkProps, viewProps])
|
|
24
|
+
|
|
25
|
+
const selectPressableStyles = (tokens, widthVariant, equalWidth) => {
|
|
26
|
+
const styles = {
|
|
27
|
+
borderColor: tokens.borderColor,
|
|
28
|
+
borderRadius: tokens.borderRadius,
|
|
29
|
+
borderWidth: tokens.borderWidth,
|
|
30
|
+
...Platform.select({
|
|
31
|
+
web: {
|
|
32
|
+
outline: 'none'
|
|
33
|
+
}
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (widthVariant === DYNAMIC_WIDTH_VARIANT) {
|
|
38
|
+
styles.width = 'auto'
|
|
39
|
+
} else if (widthVariant === EQUAL_WIDTH_VARIANT) {
|
|
40
|
+
if (equalWidth) {
|
|
41
|
+
styles.width = equalWidth
|
|
42
|
+
} else {
|
|
43
|
+
styles.minWidth = tokens.width
|
|
44
|
+
}
|
|
45
|
+
} else {
|
|
46
|
+
styles.width = tokens.width
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return styles
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const selectIconContainerStyles = (tokens) => ({
|
|
53
|
+
paddingBottom: tokens.iconContainerPaddingBottom,
|
|
54
|
+
paddingLeft: tokens.iconContainerPaddingLeft,
|
|
55
|
+
paddingRight: tokens.iconContainerPaddingRight,
|
|
56
|
+
paddingTop: tokens.iconContainerPaddingTop
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
const selectIconVariant = () => ({
|
|
60
|
+
background: true,
|
|
61
|
+
padding: 'medium'
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
const selectIconTokens = (tokens) => ({
|
|
65
|
+
backgroundColor: tokens.iconBackgroundColor,
|
|
66
|
+
color: tokens.iconColor,
|
|
67
|
+
size: tokens.iconSize,
|
|
68
|
+
width: tokens.iconWidth
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
const selectImageStyles = (tokens) => ({
|
|
72
|
+
width: tokens.imageWidth,
|
|
73
|
+
height: tokens.imageHeight
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
const selectLabelContainerStyles = (tokens) => ({
|
|
77
|
+
paddingBottom: tokens.labelContainerPaddingBottom,
|
|
78
|
+
paddingLeft: tokens.labelContainerPaddingLeft,
|
|
79
|
+
paddingRight: tokens.labelContainerPaddingRight,
|
|
80
|
+
paddingTop: tokens.labelContainerPaddingTop
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
const selectTitleTextStyles = (tokens) =>
|
|
84
|
+
applyTextStyles({
|
|
85
|
+
fontColor: tokens.labelFontColor,
|
|
86
|
+
fontName: tokens.labelFontName,
|
|
87
|
+
fontSize: tokens.labelFontSize,
|
|
88
|
+
fontWeight: tokens.labelFontWeight,
|
|
89
|
+
lineHeight: tokens.labelLineHeight,
|
|
90
|
+
textDecorationLine: tokens.labelUnderline,
|
|
91
|
+
textAlign: tokens.labelTextAlign
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* A clickable shortcut item component that displays an icon or image with an optional label.
|
|
96
|
+
* Can be used within a Shortcuts container to create a grid of navigation shortcuts.
|
|
97
|
+
*
|
|
98
|
+
* @component
|
|
99
|
+
* @param {Object} props - Component props
|
|
100
|
+
* @param {string} [props.icon] - Icon identifier to display
|
|
101
|
+
* @param {Object} [props.image={ src: '', alt: '' }] - Image object with src and alt properties
|
|
102
|
+
* @param {string} [props.image.src] - Image source URL
|
|
103
|
+
* @param {string} [props.image.alt] - Image alt text for accessibility
|
|
104
|
+
* @param {string|React.ReactNode} props.label - Label text or content to display below the icon/image
|
|
105
|
+
* @param {boolean} [props.hideLabel=false] - Whether to hide the label for this specific item
|
|
106
|
+
* @param {string} [props.href] - Link URL for navigation
|
|
107
|
+
* @param {Object} [props.iconVariant] - Icon variant to apply to this specific item
|
|
108
|
+
* @param {Object} [props.tokens] - Theme tokens to customize appearance
|
|
109
|
+
* @param {Object} [props.variant] - Variant configuration object for this specific item
|
|
110
|
+
* @param {string} [props.variant.width] - Width variant (e.g., 'dynamic', 'equal')
|
|
111
|
+
* @param {Function} [props.onPressableStateChange] - Callback function that receives the pressable state object (pressed, hovered, focused)
|
|
112
|
+
* @param {number} [props.maxWidth] - Maximum width for equal width variant (injected by Shortcuts container)
|
|
113
|
+
* @param {Function} [props.registerWidth] - Callback to register width for equal width variant (injected by Shortcuts container)
|
|
114
|
+
* @param {Object} [props.containerVariant] - Variant configuration from Shortcuts container (injected by Shortcuts container)
|
|
115
|
+
* @param {boolean} [props.containerHideLabels] - Hide labels setting from Shortcuts container (injected by Shortcuts container)
|
|
116
|
+
* @param {Object} [props.containerIconVariant] - Icon variant from Shortcuts container (injected by Shortcuts container)
|
|
117
|
+
* @param {React.Ref} ref - Forwarded ref to the Pressable component
|
|
118
|
+
* @returns {React.ReactElement} The rendered shortcut item
|
|
119
|
+
*
|
|
120
|
+
* @example
|
|
121
|
+
* <ShortcutsItem
|
|
122
|
+
* icon={HomeIcon}
|
|
123
|
+
* label="Home"
|
|
124
|
+
* href="/home"
|
|
125
|
+
* onPressableStateChange={(state) => console.log(state)}
|
|
126
|
+
* />
|
|
127
|
+
*/
|
|
128
|
+
const ShortcutsItem = React.forwardRef(
|
|
129
|
+
(
|
|
130
|
+
{
|
|
131
|
+
icon,
|
|
132
|
+
image = { src: '', alt: '' },
|
|
133
|
+
label,
|
|
134
|
+
hideLabel = false,
|
|
135
|
+
href,
|
|
136
|
+
iconVariant,
|
|
137
|
+
tokens,
|
|
138
|
+
variant,
|
|
139
|
+
onPressableStateChange,
|
|
140
|
+
maxWidth,
|
|
141
|
+
registerWidth,
|
|
142
|
+
containerVariant,
|
|
143
|
+
containerHideLabels,
|
|
144
|
+
containerIconVariant,
|
|
145
|
+
...rest
|
|
146
|
+
},
|
|
147
|
+
ref
|
|
148
|
+
) => {
|
|
149
|
+
const mergedVariant = { ...containerVariant, ...variant }
|
|
150
|
+
const widthVariant = mergedVariant?.width
|
|
151
|
+
const shouldHideLabel = hideLabel || containerHideLabels
|
|
152
|
+
const mergedIconVariant = iconVariant ?? containerIconVariant
|
|
153
|
+
|
|
154
|
+
const getThemeTokens = useThemeTokensCallback('ShortcutsItem', tokens, mergedVariant)
|
|
155
|
+
const getTokens = (pressableState) => getThemeTokens(resolvePressableState(pressableState))
|
|
156
|
+
|
|
157
|
+
const { onPress, ...props } = clickProps.toPressProps(rest)
|
|
158
|
+
const { hrefAttrs, rawRest } = hrefAttrsProp.bundle(props)
|
|
159
|
+
const selectedProps = selectProps({
|
|
160
|
+
href,
|
|
161
|
+
onPress: linkProps.handleHref({ href, onPress }),
|
|
162
|
+
hrefAttrs,
|
|
163
|
+
...rawRest
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
const handleLayout = (event) => {
|
|
167
|
+
if (widthVariant === EQUAL_WIDTH_VARIANT && registerWidth) {
|
|
168
|
+
const { width } = event.nativeEvent.layout
|
|
169
|
+
registerWidth(width)
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return (
|
|
174
|
+
<Pressable
|
|
175
|
+
ref={ref}
|
|
176
|
+
style={(pressableState) =>
|
|
177
|
+
selectPressableStyles(getTokens(pressableState), widthVariant, maxWidth)
|
|
178
|
+
}
|
|
179
|
+
onLayout={handleLayout}
|
|
180
|
+
{...selectedProps}
|
|
181
|
+
>
|
|
182
|
+
{(pressableState) => {
|
|
183
|
+
const themeTokens = getTokens(pressableState)
|
|
184
|
+
|
|
185
|
+
if (onPressableStateChange) {
|
|
186
|
+
onPressableStateChange(resolvePressableState(pressableState))
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return (
|
|
190
|
+
<View style={staticStyles.container}>
|
|
191
|
+
{icon && (
|
|
192
|
+
<View style={selectIconContainerStyles(themeTokens)}>
|
|
193
|
+
<Icon
|
|
194
|
+
icon={icon}
|
|
195
|
+
variant={mergedIconVariant ?? selectIconVariant()}
|
|
196
|
+
tokens={mergedIconVariant ? {} : selectIconTokens(themeTokens)}
|
|
197
|
+
{...(Platform.OS === 'web' && { accessibilityLabel: label })}
|
|
198
|
+
/>
|
|
199
|
+
</View>
|
|
200
|
+
)}
|
|
201
|
+
{!icon && image && (
|
|
202
|
+
<Image
|
|
203
|
+
source={image.src}
|
|
204
|
+
alt={image.alt}
|
|
205
|
+
style={selectImageStyles(themeTokens)}
|
|
206
|
+
resizeMethod="resize"
|
|
207
|
+
accessibilityIgnoresInvertColors
|
|
208
|
+
/>
|
|
209
|
+
)}
|
|
210
|
+
{label && !shouldHideLabel && (
|
|
211
|
+
<View style={[staticStyles.label, selectLabelContainerStyles(themeTokens)]}>
|
|
212
|
+
{wrapStringsInText(label, { style: selectTitleTextStyles(themeTokens) })}
|
|
213
|
+
</View>
|
|
214
|
+
)}
|
|
215
|
+
</View>
|
|
216
|
+
)
|
|
217
|
+
}}
|
|
218
|
+
</Pressable>
|
|
219
|
+
)
|
|
220
|
+
}
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
ShortcutsItem.displayName = 'ShortcutsItem'
|
|
224
|
+
|
|
225
|
+
ShortcutsItem.propTypes = {
|
|
226
|
+
...selectedSystemPropTypes,
|
|
227
|
+
tokens: getTokensPropType('ShortcutsItem'),
|
|
228
|
+
variant: variantProp.propType,
|
|
229
|
+
/**
|
|
230
|
+
* Icon for the ShortcutsItem
|
|
231
|
+
*/
|
|
232
|
+
icon: PropTypes.elementType,
|
|
233
|
+
/**
|
|
234
|
+
* Image for the ShortcutsItem
|
|
235
|
+
*/
|
|
236
|
+
image: PropTypes.shape({
|
|
237
|
+
src: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.object]),
|
|
238
|
+
alt: PropTypes.string
|
|
239
|
+
}),
|
|
240
|
+
/**
|
|
241
|
+
* Label for the ShortcutsItem
|
|
242
|
+
*/
|
|
243
|
+
label: PropTypes.string,
|
|
244
|
+
/**
|
|
245
|
+
* Hide the label for this specific ShortcutsItem. When true, the label is visually hidden but remains accessible to screen readers via the icon's accessibilityLabel.
|
|
246
|
+
*/
|
|
247
|
+
hideLabel: PropTypes.bool,
|
|
248
|
+
/**
|
|
249
|
+
* href for the ShortcutsItem
|
|
250
|
+
*/
|
|
251
|
+
href: PropTypes.string,
|
|
252
|
+
/**
|
|
253
|
+
* Icon variant for this specific ShortcutsItem
|
|
254
|
+
*/
|
|
255
|
+
iconVariant: variantProp.propType,
|
|
256
|
+
/**
|
|
257
|
+
* Callback function that receives the pressable state object containing pressed, hovered, and focused boolean properties
|
|
258
|
+
*/
|
|
259
|
+
onPressableStateChange: PropTypes.func,
|
|
260
|
+
/**
|
|
261
|
+
* Maximum width for equal width variant (automatically injected by Shortcuts container)
|
|
262
|
+
* @private
|
|
263
|
+
*/
|
|
264
|
+
maxWidth: PropTypes.number,
|
|
265
|
+
/**
|
|
266
|
+
* Callback to register width for equal width variant (automatically injected by Shortcuts container)
|
|
267
|
+
* @private
|
|
268
|
+
*/
|
|
269
|
+
registerWidth: PropTypes.func,
|
|
270
|
+
/**
|
|
271
|
+
* Variant configuration from Shortcuts container (automatically injected by Shortcuts container)
|
|
272
|
+
* @private
|
|
273
|
+
*/
|
|
274
|
+
containerVariant: variantProp.propType,
|
|
275
|
+
/**
|
|
276
|
+
* Hide labels setting from Shortcuts container (automatically injected by Shortcuts container)
|
|
277
|
+
* @private
|
|
278
|
+
*/
|
|
279
|
+
containerHideLabels: PropTypes.bool,
|
|
280
|
+
/**
|
|
281
|
+
* Icon variant from Shortcuts container (automatically injected by Shortcuts container)
|
|
282
|
+
* @private
|
|
283
|
+
*/
|
|
284
|
+
containerIconVariant: variantProp.propType
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const staticStyles = StyleSheet.create({
|
|
288
|
+
container: {
|
|
289
|
+
alignItems: 'center',
|
|
290
|
+
justifyContent: 'center'
|
|
291
|
+
},
|
|
292
|
+
label: {
|
|
293
|
+
flexWrap: 'wrap'
|
|
294
|
+
}
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
export default ShortcutsItem
|
|
@@ -263,6 +263,10 @@ const TextInputBase = React.forwardRef(
|
|
|
263
263
|
// Add a space every 4 digits starting from the 5th position
|
|
264
264
|
filteredText = formattedValue.replace(regex, '$1 ').trim()
|
|
265
265
|
}
|
|
266
|
+
// Apply maxLength if provided
|
|
267
|
+
if (rest.maxLength && filteredText && filteredText.length > rest.maxLength) {
|
|
268
|
+
filteredText = filteredText.substring(0, rest.maxLength)
|
|
269
|
+
}
|
|
266
270
|
setValue(filteredText, event)
|
|
267
271
|
if (typeof onChangeText === 'function') onChangeText(filteredText, event)
|
|
268
272
|
}
|
|
@@ -340,7 +344,7 @@ const TextInputBase = React.forwardRef(
|
|
|
340
344
|
onMouseOut: handleMouseOut,
|
|
341
345
|
onChange: handleChangeText,
|
|
342
346
|
defaultValue: initialValue,
|
|
343
|
-
maxLength: type === 'card' ? 19 :
|
|
347
|
+
maxLength: type === 'card' ? 19 : rest.maxLength,
|
|
344
348
|
value: isControlled ? currentValue : undefined,
|
|
345
349
|
onKeyPress
|
|
346
350
|
}
|