@telus-uds/components-base 1.19.0 → 1.21.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/__tests17__/ThemeProvider/ThemeProvider.test.jsx +3 -1
- package/component-docs.json +838 -125
- package/lib/BaseProvider/index.js +2 -1
- package/lib/Box/Box.js +14 -1
- package/lib/Button/ButtonDropdown.js +207 -0
- package/lib/Button/index.js +8 -0
- package/lib/Carousel/Carousel.js +2 -2
- package/lib/Carousel/CarouselItem/CarouselItem.js +7 -1
- package/lib/Carousel/CarouselTabs/CarouselTabsPanel.js +21 -4
- package/lib/FlexGrid/Col/Col.js +1 -3
- package/lib/FlexGrid/FlexGrid.js +3 -5
- package/lib/FlexGrid/Row/Row.js +3 -3
- package/lib/IconButton/IconButton.js +12 -4
- package/lib/MultiSelectFilter/MultiSelectFilter.js +276 -0
- package/lib/MultiSelectFilter/dictionary.js +19 -0
- package/lib/MultiSelectFilter/index.js +13 -0
- package/lib/Search/Search.js +4 -1
- package/lib/Select/Picker.native.js +16 -13
- package/lib/Select/Select.js +7 -1
- package/lib/Select/constants.js +15 -0
- package/lib/StepTracker/Step.js +2 -1
- package/lib/TextInput/TextInput.js +9 -2
- package/lib/TextInput/TextInputBase.js +52 -8
- package/lib/TextInput/dictionary.js +15 -0
- package/lib/ThemeProvider/ThemeProvider.js +24 -7
- package/lib/ThemeProvider/utils/styles.js +3 -1
- package/lib/index.js +18 -0
- package/lib/utils/BaseView/BaseView.js +64 -0
- package/lib/utils/BaseView/BaseView.native.js +16 -0
- package/lib/utils/BaseView/index.js +13 -0
- package/lib/utils/index.js +10 -1
- package/lib/utils/input.js +11 -3
- package/lib/utils/props/handlerProps.js +5 -0
- package/lib-module/BaseProvider/index.js +2 -1
- package/lib-module/Box/Box.js +14 -1
- package/lib-module/Button/ButtonDropdown.js +181 -0
- package/lib-module/Button/index.js +2 -1
- package/lib-module/Carousel/Carousel.js +2 -2
- package/lib-module/Carousel/CarouselItem/CarouselItem.js +8 -2
- package/lib-module/Carousel/CarouselTabs/CarouselTabsPanel.js +23 -6
- package/lib-module/FlexGrid/Col/Col.js +2 -3
- package/lib-module/FlexGrid/FlexGrid.js +2 -3
- package/lib-module/FlexGrid/Row/Row.js +2 -2
- package/lib-module/IconButton/IconButton.js +14 -4
- package/lib-module/MultiSelectFilter/MultiSelectFilter.js +248 -0
- package/lib-module/MultiSelectFilter/dictionary.js +12 -0
- package/lib-module/MultiSelectFilter/index.js +2 -0
- package/lib-module/Search/Search.js +4 -1
- package/lib-module/Select/Picker.native.js +15 -13
- package/lib-module/Select/Select.js +6 -1
- package/lib-module/Select/constants.js +5 -0
- package/lib-module/StepTracker/Step.js +2 -1
- package/lib-module/TextInput/TextInput.js +6 -0
- package/lib-module/TextInput/TextInputBase.js +52 -10
- package/lib-module/TextInput/dictionary.js +8 -0
- package/lib-module/ThemeProvider/ThemeProvider.js +24 -7
- package/lib-module/ThemeProvider/utils/styles.js +3 -1
- package/lib-module/index.js +2 -0
- package/lib-module/utils/BaseView/BaseView.js +43 -0
- package/lib-module/utils/BaseView/BaseView.native.js +6 -0
- package/lib-module/utils/BaseView/index.js +2 -0
- package/lib-module/utils/index.js +2 -1
- package/lib-module/utils/input.js +11 -3
- package/lib-module/utils/props/handlerProps.js +5 -0
- package/package.json +3 -3
- package/src/BaseProvider/index.jsx +4 -1
- package/src/Box/Box.jsx +14 -1
- package/src/Button/ButtonDropdown.jsx +179 -0
- package/src/Button/index.js +2 -1
- package/src/Carousel/Carousel.jsx +6 -3
- package/src/Carousel/CarouselItem/CarouselItem.jsx +9 -2
- package/src/Carousel/CarouselTabs/CarouselTabsPanel.jsx +19 -5
- package/src/FlexGrid/Col/Col.jsx +4 -4
- package/src/FlexGrid/FlexGrid.jsx +11 -10
- package/src/FlexGrid/Row/Row.jsx +4 -3
- package/src/IconButton/IconButton.jsx +3 -1
- package/src/MultiSelectFilter/MultiSelectFilter.jsx +227 -0
- package/src/MultiSelectFilter/dictionary.js +12 -0
- package/src/MultiSelectFilter/index.js +3 -0
- package/src/Search/Search.jsx +2 -1
- package/src/Select/Picker.native.jsx +29 -14
- package/src/Select/Select.jsx +7 -1
- package/src/Select/constants.js +5 -0
- package/src/StepTracker/Step.jsx +5 -1
- package/src/TextInput/TextInput.jsx +5 -0
- package/src/TextInput/TextInputBase.jsx +43 -8
- package/src/TextInput/dictionary.js +8 -0
- package/src/ThemeProvider/ThemeProvider.jsx +23 -6
- package/src/ThemeProvider/utils/styles.js +3 -1
- package/src/index.js +2 -0
- package/src/utils/BaseView/BaseView.jsx +38 -0
- package/src/utils/BaseView/BaseView.native.jsx +6 -0
- package/src/utils/BaseView/index.js +3 -0
- package/src/utils/index.js +1 -0
- package/src/utils/input.js +9 -4
- package/src/utils/props/handlerProps.js +4 -0
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import React, { forwardRef, useState } from 'react'
|
|
2
|
+
import PropTypes from 'prop-types'
|
|
3
|
+
|
|
4
|
+
import { useThemeTokensCallback } from '../ThemeProvider'
|
|
5
|
+
import {
|
|
6
|
+
containUniqueFields,
|
|
7
|
+
getTokensPropType,
|
|
8
|
+
getPressHandlersWithArgs,
|
|
9
|
+
selectTokens,
|
|
10
|
+
useCopy,
|
|
11
|
+
useMultipleInputValues,
|
|
12
|
+
variantProp
|
|
13
|
+
} from '../utils'
|
|
14
|
+
import dictionary from './dictionary'
|
|
15
|
+
|
|
16
|
+
import Box from '../Box'
|
|
17
|
+
import { Button, ButtonDropdown } from '../Button'
|
|
18
|
+
import { CheckboxGroup } from '../Checkbox'
|
|
19
|
+
import Divider from '../Divider'
|
|
20
|
+
import FlexGrid from '../FlexGrid'
|
|
21
|
+
import Modal from '../Modal'
|
|
22
|
+
import Spacer from '../Spacer'
|
|
23
|
+
import StackView from '../StackView'
|
|
24
|
+
import Typography from '../Typography'
|
|
25
|
+
import { TextButton } from '../Link'
|
|
26
|
+
|
|
27
|
+
const { Col, Row } = FlexGrid
|
|
28
|
+
|
|
29
|
+
const MultiSelectFilter = forwardRef(
|
|
30
|
+
(
|
|
31
|
+
{
|
|
32
|
+
label,
|
|
33
|
+
id = label,
|
|
34
|
+
variant,
|
|
35
|
+
tokens,
|
|
36
|
+
items = [],
|
|
37
|
+
values,
|
|
38
|
+
initialValues,
|
|
39
|
+
maxValues,
|
|
40
|
+
onChange,
|
|
41
|
+
copy = 'en',
|
|
42
|
+
readOnly = false,
|
|
43
|
+
inactive = false,
|
|
44
|
+
rowLimit = 12,
|
|
45
|
+
...rest
|
|
46
|
+
},
|
|
47
|
+
ref
|
|
48
|
+
) => {
|
|
49
|
+
const { currentValues, setValues } = useMultipleInputValues({
|
|
50
|
+
initialValues,
|
|
51
|
+
values,
|
|
52
|
+
maxValues,
|
|
53
|
+
onChange,
|
|
54
|
+
readOnly
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
const getItemTokens = useThemeTokensCallback('ButtonDropdown', tokens, variant)
|
|
58
|
+
const getButtonTokens = (buttonState) => selectTokens('Button', getItemTokens(buttonState))
|
|
59
|
+
const getCopy = useCopy({ dictionary, copy })
|
|
60
|
+
|
|
61
|
+
const [isOpen, setIsOpen] = useState(false)
|
|
62
|
+
const [checkedIds, setCheckedIds] = useState(currentValues ?? [])
|
|
63
|
+
|
|
64
|
+
const colSize = items.length > rowLimit ? 2 : 1
|
|
65
|
+
const isSelected = currentValues.length > 0
|
|
66
|
+
|
|
67
|
+
const uniqueFields = ['id', 'label']
|
|
68
|
+
if (!containUniqueFields(items, uniqueFields)) {
|
|
69
|
+
throw new Error(`MultiSelectFilter items must have unique ${uniqueFields.join(', ')}`)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Pass an object of relevant component state as first argument for any passed-in press handlers
|
|
73
|
+
const pressHandlers = getPressHandlersWithArgs(rest, [{ id, label, currentValues }])
|
|
74
|
+
|
|
75
|
+
const handleChange = (event) => {
|
|
76
|
+
if (pressHandlers.onPress) pressHandlers?.onPress(event)
|
|
77
|
+
setIsOpen(true)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const onApply = (e) => {
|
|
81
|
+
setValues(e)
|
|
82
|
+
setIsOpen(false)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<>
|
|
87
|
+
<Modal
|
|
88
|
+
isOpen={isOpen}
|
|
89
|
+
onClose={() => setIsOpen(false)}
|
|
90
|
+
variant={{ width: colSize > 1 ? 'size576' : 's' }}
|
|
91
|
+
>
|
|
92
|
+
<Row>
|
|
93
|
+
<Typography variant={{ size: 'h4' }}>
|
|
94
|
+
{getCopy('filterByLabel').replace(/%\{filterCategory\}/g, label.toLowerCase())}
|
|
95
|
+
</Typography>
|
|
96
|
+
</Row>
|
|
97
|
+
<Spacer space={4} />
|
|
98
|
+
<Spacer space={1} />
|
|
99
|
+
<Box scroll={true}>
|
|
100
|
+
<Row distribute="between">
|
|
101
|
+
{[...Array(colSize).keys()].map((i) => (
|
|
102
|
+
<Col xs={12 / colSize} key={i}>
|
|
103
|
+
<CheckboxGroup
|
|
104
|
+
items={items.slice(i * rowLimit, (i + 1) * rowLimit)}
|
|
105
|
+
checkedIds={checkedIds}
|
|
106
|
+
onChange={(e) => setCheckedIds(e, i)}
|
|
107
|
+
/>
|
|
108
|
+
<Spacer size={4} />
|
|
109
|
+
</Col>
|
|
110
|
+
))}
|
|
111
|
+
</Row>
|
|
112
|
+
</Box>
|
|
113
|
+
<Divider
|
|
114
|
+
variant={{ width: 'full', color: 'E3E6E8', decorative: true, weight: 'thin' }}
|
|
115
|
+
space={4}
|
|
116
|
+
/>
|
|
117
|
+
<Row>
|
|
118
|
+
<StackView direction="row" space={3} tokens={{ alignItems: 'center' }}>
|
|
119
|
+
<Button
|
|
120
|
+
onPress={() => onApply(checkedIds)}
|
|
121
|
+
variant={{ size: 'small', priority: 'high' }}
|
|
122
|
+
>
|
|
123
|
+
{getCopy('applyButtonLabel')}
|
|
124
|
+
</Button>
|
|
125
|
+
<Box>
|
|
126
|
+
<TextButton onPress={() => setCheckedIds([])}>
|
|
127
|
+
{getCopy('clearButtonLabel')}
|
|
128
|
+
</TextButton>
|
|
129
|
+
</Box>
|
|
130
|
+
</StackView>
|
|
131
|
+
</Row>
|
|
132
|
+
</Modal>
|
|
133
|
+
<ButtonDropdown
|
|
134
|
+
ref={ref}
|
|
135
|
+
key={id}
|
|
136
|
+
{...pressHandlers}
|
|
137
|
+
value={isOpen}
|
|
138
|
+
selected={isSelected}
|
|
139
|
+
label={label}
|
|
140
|
+
onChange={handleChange}
|
|
141
|
+
tokens={getButtonTokens}
|
|
142
|
+
inactive={inactive}
|
|
143
|
+
/>
|
|
144
|
+
</>
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
)
|
|
148
|
+
MultiSelectFilter.displayName = 'MultiSelectFilter'
|
|
149
|
+
|
|
150
|
+
MultiSelectFilter.propTypes = {
|
|
151
|
+
/**
|
|
152
|
+
* The text displayed to the user in a ButtonDropdown.
|
|
153
|
+
*/
|
|
154
|
+
label: PropTypes.string.isRequired,
|
|
155
|
+
/**
|
|
156
|
+
* An optional unique string may be provided to identify the ButtonDropdown.
|
|
157
|
+
* If not provided, the label is used.
|
|
158
|
+
*/
|
|
159
|
+
id: PropTypes.string,
|
|
160
|
+
/**
|
|
161
|
+
* Sets the variant for ButtonDropdown element.
|
|
162
|
+
*/
|
|
163
|
+
variant: variantProp.propType,
|
|
164
|
+
/**
|
|
165
|
+
* Sets the tokens for ButtonDropdown element.
|
|
166
|
+
*/
|
|
167
|
+
tokens: getTokensPropType('ButtonDropdown'),
|
|
168
|
+
/**
|
|
169
|
+
* The options a user may select.
|
|
170
|
+
*/
|
|
171
|
+
items: PropTypes.arrayOf(
|
|
172
|
+
PropTypes.shape({
|
|
173
|
+
/**
|
|
174
|
+
* The text displayed to the user with a checkbox, describing this option.
|
|
175
|
+
*/
|
|
176
|
+
label: PropTypes.string.isRequired,
|
|
177
|
+
/**
|
|
178
|
+
* An optional unique string may be provided to identify this option.
|
|
179
|
+
* If not provided, the label is used.
|
|
180
|
+
*/
|
|
181
|
+
id: PropTypes.string
|
|
182
|
+
})
|
|
183
|
+
),
|
|
184
|
+
/**
|
|
185
|
+
* If the selected item(s) in the checkbox group(s) are to be controlled externally by
|
|
186
|
+
* a parent component, pass an array of strings as well as an `onChange` handler.
|
|
187
|
+
* Passing an array for "values" makes the MultiSelectFilter a "controlled" component that
|
|
188
|
+
* expects its state to be handled via `onChange` and so doesn't handle it itself.
|
|
189
|
+
*/
|
|
190
|
+
values: PropTypes.arrayOf(PropTypes.string),
|
|
191
|
+
/**
|
|
192
|
+
* If `values` is not passed, making the MultiSelectFilter an "uncontrolled" component
|
|
193
|
+
* managing its own selected state, a default set of selections may be provided.
|
|
194
|
+
* Changing the `initialValues` does not change the user's selections.
|
|
195
|
+
*/
|
|
196
|
+
initialValues: PropTypes.arrayOf(PropTypes.string),
|
|
197
|
+
/**
|
|
198
|
+
* If provided, sets a maximum number of items a user may select at once.
|
|
199
|
+
*/
|
|
200
|
+
maxValues: PropTypes.number,
|
|
201
|
+
/**
|
|
202
|
+
* If provided, this function is called when the current selection is changed
|
|
203
|
+
* and is passed an array of the `id`s of all currently selected `items`.
|
|
204
|
+
*/
|
|
205
|
+
onChange: PropTypes.func,
|
|
206
|
+
/**
|
|
207
|
+
* Select English or French copy for the accessible label.
|
|
208
|
+
*/
|
|
209
|
+
copy: PropTypes.oneOf(['en', 'fr']),
|
|
210
|
+
/**
|
|
211
|
+
* If true, the ButtonDropdown cannot be selected by the user and simply show their current state.
|
|
212
|
+
*/
|
|
213
|
+
readOnly: PropTypes.string,
|
|
214
|
+
/**
|
|
215
|
+
* If true, the MultiSelectFilter cannot be interacted with, ButtonDropdown is
|
|
216
|
+
* set as `disabled` and if the theme supports `inactive` appearances rules, these
|
|
217
|
+
* are applied.
|
|
218
|
+
*/
|
|
219
|
+
inactive: PropTypes.string,
|
|
220
|
+
/**
|
|
221
|
+
* Sets the maximum number of items in one column. If number of items are more
|
|
222
|
+
* than the `rowLimit`, they will be rendered in 2 columns.
|
|
223
|
+
*/
|
|
224
|
+
rowLimit: PropTypes.number
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export default MultiSelectFilter
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
en: {
|
|
3
|
+
filterByLabel: 'Filter by %{filterCategory}:',
|
|
4
|
+
applyButtonLabel: 'Apply',
|
|
5
|
+
clearButtonLabel: 'Clear'
|
|
6
|
+
},
|
|
7
|
+
fr: {
|
|
8
|
+
filterByLabel: 'Filtrer par %{filterCategory}:',
|
|
9
|
+
applyButtonLabel: 'Appliquer',
|
|
10
|
+
clearButtonLabel: 'Effacer'
|
|
11
|
+
}
|
|
12
|
+
}
|
package/src/Search/Search.jsx
CHANGED
|
@@ -6,28 +6,43 @@ import NativePicker from 'react-native-picker-select'
|
|
|
6
6
|
import { a11yProps, componentPropType } from '../utils'
|
|
7
7
|
import Group from './Group'
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}) => ({
|
|
17
|
-
height
|
|
9
|
+
import {
|
|
10
|
+
ANDROID_HEIGHT_OFFSET,
|
|
11
|
+
ANDROID_HORIZONTAL_PADDING_OFFSET,
|
|
12
|
+
ANDROID_DEFAULT_PADDING
|
|
13
|
+
} from './constants'
|
|
14
|
+
|
|
15
|
+
// Styling of the native input is very limited, most of the styles have to be applied to an additional View
|
|
16
|
+
const selectAndroidInputStyles = ({ height = 0, color }) => ({
|
|
17
|
+
height,
|
|
18
|
+
paddingBottom: 0,
|
|
19
|
+
paddingTop: 0,
|
|
18
20
|
color
|
|
19
21
|
})
|
|
20
22
|
|
|
21
|
-
//
|
|
22
|
-
const selectAndroidContainerStyles = ({
|
|
23
|
-
paddingLeft
|
|
24
|
-
paddingRight
|
|
23
|
+
// The native input has a side padding of 8px, which can't be adjusted, so we have to account for that in the container
|
|
24
|
+
const selectAndroidContainerStyles = ({
|
|
25
|
+
paddingLeft = ANDROID_DEFAULT_PADDING,
|
|
26
|
+
paddingRight = ANDROID_DEFAULT_PADDING,
|
|
25
27
|
...rest
|
|
28
|
+
}) => ({
|
|
29
|
+
...rest,
|
|
30
|
+
paddingLeft:
|
|
31
|
+
paddingLeft > ANDROID_HORIZONTAL_PADDING_OFFSET
|
|
32
|
+
? paddingLeft - ANDROID_HORIZONTAL_PADDING_OFFSET
|
|
33
|
+
: ANDROID_DEFAULT_PADDING,
|
|
34
|
+
paddingRight:
|
|
35
|
+
paddingRight > ANDROID_HORIZONTAL_PADDING_OFFSET
|
|
36
|
+
? paddingRight - ANDROID_HORIZONTAL_PADDING_OFFSET
|
|
37
|
+
: ANDROID_DEFAULT_PADDING,
|
|
38
|
+
paddingBottom: ANDROID_DEFAULT_PADDING,
|
|
39
|
+
paddingTop: ANDROID_DEFAULT_PADDING,
|
|
40
|
+
height: rest.height + ANDROID_HEIGHT_OFFSET
|
|
26
41
|
})
|
|
27
42
|
|
|
28
43
|
const Picker = forwardRef(
|
|
29
44
|
({ value, onChange, onFocus, onBlur, style, inactive, children, placeholder, ...rest }, ref) => {
|
|
30
|
-
//
|
|
45
|
+
// Ungroup items, since there's no way to support groups on native
|
|
31
46
|
const flatChildren = Children.toArray(children).flatMap((child) => {
|
|
32
47
|
if (child.type === Group) {
|
|
33
48
|
return child.props.children
|
package/src/Select/Select.jsx
CHANGED
|
@@ -16,6 +16,8 @@ import {
|
|
|
16
16
|
import Picker from './Picker'
|
|
17
17
|
import InputSupports from '../InputSupports'
|
|
18
18
|
|
|
19
|
+
import { ANDROID_VALIDATION_ICON_CONTAINER_OFFSET } from './constants'
|
|
20
|
+
|
|
19
21
|
const [selectProps, selectedSystemPropTypes] = selectSystemProps([
|
|
20
22
|
a11yProps,
|
|
21
23
|
inputSupportsProps,
|
|
@@ -133,7 +135,11 @@ const selectValidationIconContainerStyles = ({
|
|
|
133
135
|
paddingBottom
|
|
134
136
|
}) => ({
|
|
135
137
|
paddingRight: icon ? paddingRight + iconSize : paddingRight,
|
|
136
|
-
|
|
138
|
+
...(Platform.OS === 'android'
|
|
139
|
+
? {
|
|
140
|
+
paddingBottom: paddingBottom + ANDROID_VALIDATION_ICON_CONTAINER_OFFSET
|
|
141
|
+
}
|
|
142
|
+
: { paddingBottom })
|
|
137
143
|
})
|
|
138
144
|
|
|
139
145
|
/**
|
package/src/StepTracker/Step.jsx
CHANGED
|
@@ -138,7 +138,11 @@ const Step = ({ label, name, status = 0, stepCount = 0, stepIndex = 0, tokens, .
|
|
|
138
138
|
accessibilityCurrent={status === stepIndex}
|
|
139
139
|
{...selectProps(rest)}
|
|
140
140
|
>
|
|
141
|
-
<StackView
|
|
141
|
+
<StackView
|
|
142
|
+
direction="row"
|
|
143
|
+
space={0}
|
|
144
|
+
tokens={{ alignItems: 'center', flexGrow: 0, justifyContent: 'center' }}
|
|
145
|
+
>
|
|
142
146
|
<View
|
|
143
147
|
style={[staticStyles.connector, !isFirst && selectConnectorStyles(themeTokens, isActive)]}
|
|
144
148
|
/>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import React, { forwardRef } from 'react'
|
|
2
|
+
import PropTypes from 'prop-types'
|
|
2
3
|
import {
|
|
3
4
|
a11yProps,
|
|
4
5
|
focusHandlerProps,
|
|
@@ -63,6 +64,10 @@ TextInput.displayName = 'TextInput'
|
|
|
63
64
|
TextInput.propTypes = {
|
|
64
65
|
...selectedSystemPropTypes,
|
|
65
66
|
...textInputPropTypes,
|
|
67
|
+
/**
|
|
68
|
+
* A callback which if provided will get a clear button rendered and will be called whenever that button gets pressed.
|
|
69
|
+
*/
|
|
70
|
+
onClear: PropTypes.func,
|
|
66
71
|
tokens: getTokensPropType('TextInput'),
|
|
67
72
|
variant: variantProp.propType
|
|
68
73
|
}
|
|
@@ -1,19 +1,22 @@
|
|
|
1
|
-
import React, { forwardRef, useEffect, useState } from 'react'
|
|
1
|
+
import React, { forwardRef, useEffect, useRef, useState } from 'react'
|
|
2
2
|
import PropTypes from 'prop-types'
|
|
3
3
|
import { Platform, StyleSheet, TextInput as NativeTextInput, View } from 'react-native'
|
|
4
4
|
import { applyTextStyles, useTheme, useThemeTokens, applyOuterBorder } from '../ThemeProvider'
|
|
5
5
|
import StackView from '../StackView'
|
|
6
|
+
import IconButton from '../IconButton'
|
|
6
7
|
import {
|
|
7
8
|
a11yProps,
|
|
8
9
|
getTokensPropType,
|
|
9
10
|
selectSystemProps,
|
|
10
11
|
textInputHandlerProps,
|
|
11
12
|
textInputProps,
|
|
13
|
+
useCopy,
|
|
12
14
|
useInputValue,
|
|
13
15
|
useSpacingScale,
|
|
14
16
|
variantProp,
|
|
15
17
|
viewProps
|
|
16
18
|
} from '../utils'
|
|
19
|
+
import dictionary from './dictionary'
|
|
17
20
|
|
|
18
21
|
const [selectProps, selectedSystemPropTypes] = selectSystemProps([
|
|
19
22
|
a11yProps,
|
|
@@ -133,12 +136,14 @@ const TextInputBase = forwardRef(
|
|
|
133
136
|
(
|
|
134
137
|
{
|
|
135
138
|
buttons = [],
|
|
139
|
+
copy = 'en',
|
|
136
140
|
height,
|
|
137
141
|
inactive,
|
|
138
142
|
initialValue,
|
|
139
143
|
onBlur,
|
|
140
144
|
onChange,
|
|
141
145
|
onChangeText,
|
|
146
|
+
onClear,
|
|
142
147
|
onFocus,
|
|
143
148
|
onMouseOut,
|
|
144
149
|
onMouseOver,
|
|
@@ -171,14 +176,18 @@ const TextInputBase = forwardRef(
|
|
|
171
176
|
if (typeof onMouseOut === 'function') onMouseOut(event)
|
|
172
177
|
}
|
|
173
178
|
|
|
174
|
-
const
|
|
179
|
+
const defaultRef = useRef()
|
|
180
|
+
const inputRef = ref ?? defaultRef
|
|
181
|
+
|
|
182
|
+
const { currentValue, resetValue, setValue, isControlled, isDirty } = useInputValue({
|
|
175
183
|
value,
|
|
176
184
|
initialValue,
|
|
185
|
+
inputRef,
|
|
177
186
|
onChange,
|
|
178
187
|
readOnly
|
|
179
188
|
})
|
|
180
189
|
|
|
181
|
-
const element =
|
|
190
|
+
const element = inputRef?.current
|
|
182
191
|
useEffect(() => {
|
|
183
192
|
if (Platform.OS === 'web' && pattern && element) {
|
|
184
193
|
// React Native Web doesn't support `pattern`, so we have to attach it via a ref,
|
|
@@ -197,7 +206,25 @@ const TextInputBase = forwardRef(
|
|
|
197
206
|
|
|
198
207
|
const themeTokens = useThemeTokens('TextInput', tokens, variant, states)
|
|
199
208
|
|
|
200
|
-
const { icon: IconComponent
|
|
209
|
+
const { buttonsGap, clearButtonIcon: ClearButtonIcon, icon: IconComponent } = themeTokens
|
|
210
|
+
const buttonsGapSize = useSpacingScale(buttonsGap)
|
|
211
|
+
const getCopy = useCopy({ dictionary, copy })
|
|
212
|
+
if (onClear && isDirty) {
|
|
213
|
+
const handleClear = (event) => {
|
|
214
|
+
onClear?.(event)
|
|
215
|
+
resetValue(event)
|
|
216
|
+
inputRef?.current?.focus()
|
|
217
|
+
}
|
|
218
|
+
buttons?.unshift(
|
|
219
|
+
<IconButton
|
|
220
|
+
accessibilityLabel={getCopy('clearButtonAccessibilityLabel')}
|
|
221
|
+
icon={ClearButtonIcon}
|
|
222
|
+
key="clear"
|
|
223
|
+
onPress={handleClear}
|
|
224
|
+
variant={{ compact: true }}
|
|
225
|
+
/>
|
|
226
|
+
)
|
|
227
|
+
}
|
|
201
228
|
|
|
202
229
|
const inputProps = {
|
|
203
230
|
...selectProps(rest),
|
|
@@ -213,15 +240,12 @@ const TextInputBase = forwardRef(
|
|
|
213
240
|
value: isControlled ? currentValue : undefined
|
|
214
241
|
}
|
|
215
242
|
|
|
216
|
-
// Get the actual gap value for the current viewport
|
|
217
|
-
const buttonsGapSize = useSpacingScale(buttonsGap)
|
|
218
|
-
|
|
219
243
|
const { themeOptions } = useTheme()
|
|
220
244
|
const nativeInputStyle = selectInputStyles({ ...themeTokens, height }, themeOptions, inactive)
|
|
221
245
|
|
|
222
246
|
return (
|
|
223
247
|
<View style={selectOuterBorderStyles(themeTokens)}>
|
|
224
|
-
<NativeTextInput ref={
|
|
248
|
+
<NativeTextInput ref={inputRef} style={nativeInputStyle} {...inputProps} />
|
|
225
249
|
{IconComponent && (
|
|
226
250
|
<View
|
|
227
251
|
pointerEvents="none" // avoid hijacking input press events
|
|
@@ -249,12 +273,23 @@ TextInputBase.displayName = 'TextInputBase'
|
|
|
249
273
|
TextInputBase.propTypes = {
|
|
250
274
|
...selectedSystemPropTypes,
|
|
251
275
|
buttons: PropTypes.arrayOf(PropTypes.node),
|
|
276
|
+
/**
|
|
277
|
+
* Select English or French copy for the accessible labels.
|
|
278
|
+
* You may also pass in a custom dictionary object.
|
|
279
|
+
*/
|
|
280
|
+
copy: PropTypes.oneOfType([
|
|
281
|
+
PropTypes.oneOf(['en', 'fr']),
|
|
282
|
+
PropTypes.shape({
|
|
283
|
+
clearButtonAccessibilityLabel: PropTypes.string
|
|
284
|
+
})
|
|
285
|
+
]),
|
|
252
286
|
height: PropTypes.number,
|
|
253
287
|
inactive: PropTypes.bool,
|
|
254
288
|
initialValue: PropTypes.string,
|
|
255
289
|
onBlur: PropTypes.func,
|
|
256
290
|
onChange: PropTypes.func,
|
|
257
291
|
onChangeText: PropTypes.func,
|
|
292
|
+
onClear: PropTypes.func,
|
|
258
293
|
onFocus: PropTypes.func,
|
|
259
294
|
onMouseOut: PropTypes.func,
|
|
260
295
|
onMouseOver: PropTypes.func,
|
|
@@ -8,14 +8,22 @@ export const uninitialisedError = new Error('Theme context used outside of Theme
|
|
|
8
8
|
export const ThemeContext = createContext(uninitialisedError)
|
|
9
9
|
export const ThemeSetterContext = createContext(uninitialisedError)
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
// These options default to `true` in v1.x to match legacy defaults and avoid breaking changes.
|
|
12
|
+
// This should change in future major releases to become "opt-in" legacy support.
|
|
13
|
+
const defaultThemeOptions = {
|
|
14
14
|
// TODO: switch `forceAbsoluteFontSizing` to be false by default in the next major version
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
forceAbsoluteFontSizing: true,
|
|
16
|
+
// TODO: switch `forceZIndex` to be false by default in the next major version
|
|
17
|
+
forceZIndex: true,
|
|
18
|
+
// TODO: switch `enableHelmetSSR` to be false by default in the next major version
|
|
19
|
+
enableHelmetSSR: true
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const ThemeProvider = ({ children, defaultTheme, themeOptions = {} }) => {
|
|
17
23
|
const [theme, setTheme] = useState(defaultTheme)
|
|
18
24
|
|
|
25
|
+
const appliedThemeOptions = { ...defaultThemeOptions, ...themeOptions }
|
|
26
|
+
|
|
19
27
|
// Validate the theme tokens version on every render.
|
|
20
28
|
// This will intentionally break the application when attempting to use an invalid theme.
|
|
21
29
|
// This will surface an incompatibility quickly rather than allowing the potential for strange bugs due to missing or incompatible tokens.
|
|
@@ -23,7 +31,9 @@ const ThemeProvider = ({
|
|
|
23
31
|
|
|
24
32
|
return (
|
|
25
33
|
<ThemeSetterContext.Provider value={setTheme}>
|
|
26
|
-
<ThemeContext.Provider value={{ ...theme, themeOptions }}>
|
|
34
|
+
<ThemeContext.Provider value={{ ...theme, themeOptions: appliedThemeOptions }}>
|
|
35
|
+
{children}
|
|
36
|
+
</ThemeContext.Provider>
|
|
27
37
|
</ThemeSetterContext.Provider>
|
|
28
38
|
)
|
|
29
39
|
}
|
|
@@ -43,9 +53,16 @@ ThemeProvider.propTypes = {
|
|
|
43
53
|
* relative sizing (in `rem`, scales depending on the browser settings)
|
|
44
54
|
* - `contentMaxWidth`: allows configuration of the content max width to be used in components
|
|
45
55
|
* such as Footnote and Notification to avoid content to stretch width more then the page's width
|
|
56
|
+
* - `forceZIndex`: available on web only, when set to false, sets zIndex on `View` to be `auto`
|
|
57
|
+
* and when true, sets zIndex to be `0` (the default from `react-native-web`)
|
|
58
|
+
* - `enableHelmetSSR`: on Web SSR, allows React Helmet to run during server-side rendering. This should be
|
|
59
|
+
* disabled unless a web app has been specifically configured to stop React Helmet accumulating
|
|
60
|
+
* instances (which may cause a memory leak). See React Helmet's docs: https://github.com/nfl/react-helmet
|
|
46
61
|
*/
|
|
47
62
|
themeOptions: PropTypes.shape({
|
|
48
63
|
forceAbsoluteFontSizing: PropTypes.bool,
|
|
64
|
+
forceZIndex: PropTypes.bool,
|
|
65
|
+
enableHelmetSSR: PropTypes.bool,
|
|
49
66
|
contentMaxWidth: responsiveProps.getTypeOptionallyByViewport(PropTypes.number)
|
|
50
67
|
})
|
|
51
68
|
}
|
|
@@ -45,7 +45,9 @@ export function applyTextStyles({
|
|
|
45
45
|
// Don't set undefined font families. May need some validation here that the font is available.
|
|
46
46
|
// Android doesn't recognise font weights natively so apply custom font weights via `fontFamily`.
|
|
47
47
|
styles.fontFamily = `${fontName}${fontWeight}${fontStyle}`
|
|
48
|
-
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (fontWeight) {
|
|
49
51
|
// If using system default font, apply the font weight directly.
|
|
50
52
|
// Font weight support in Android is limited to 'bold' or anything else === 'normal'.
|
|
51
53
|
styles.fontWeight = Platform.OS === 'android' && Number(fontWeight) > 400 ? 'bold' : fontWeight
|
package/src/index.js
CHANGED
|
@@ -21,6 +21,7 @@ export { default as InputSupports } from './InputSupports'
|
|
|
21
21
|
export * from './Link'
|
|
22
22
|
export { default as List, ListItem, ListBase } from './List'
|
|
23
23
|
export { default as Modal } from './Modal'
|
|
24
|
+
export { default as MultiSelectFilter } from './MultiSelectFilter'
|
|
24
25
|
export { default as Notification } from './Notification'
|
|
25
26
|
export { default as Pagination } from './Pagination'
|
|
26
27
|
export { default as Progress } from './Progress'
|
|
@@ -50,6 +51,7 @@ export { default as Typography } from './Typography'
|
|
|
50
51
|
|
|
51
52
|
export { default as A11yInfoProvider, useA11yInfo } from './A11yInfoProvider'
|
|
52
53
|
export { default as BaseProvider } from './BaseProvider'
|
|
54
|
+
export { useHydrationContext } from './BaseProvider/HydrationContext'
|
|
53
55
|
export { default as ViewportProvider, useViewport, ViewportContext } from './ViewportProvider'
|
|
54
56
|
export {
|
|
55
57
|
default as ThemeProvider,
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import React, { forwardRef } from 'react'
|
|
2
|
+
import { View as NativeView, StyleSheet } from 'react-native'
|
|
3
|
+
import PropTypes from 'prop-types'
|
|
4
|
+
import { useTheme } from '../../ThemeProvider'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Identical to React Native's View and supporting all the same props, but with:
|
|
8
|
+
* - a zIndex: 'auto' style added to prevent unexpectedly causing children to overlap other elements from other stacking contexts
|
|
9
|
+
*/
|
|
10
|
+
const BaseView = forwardRef(({ children, style, ...rest }, ref) => {
|
|
11
|
+
const { themeOptions } = useTheme()
|
|
12
|
+
const styleProp = Array.isArray(style) ? [...style] : [style]
|
|
13
|
+
|
|
14
|
+
if (!themeOptions.forceZIndex) {
|
|
15
|
+
styleProp.unshift(styles.resetZIndex)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<NativeView {...rest} style={styleProp} ref={ref}>
|
|
20
|
+
{children}
|
|
21
|
+
</NativeView>
|
|
22
|
+
)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
BaseView.displayName = 'BaseView'
|
|
26
|
+
|
|
27
|
+
const styles = StyleSheet.create({
|
|
28
|
+
resetZIndex: {
|
|
29
|
+
zIndex: 'auto'
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
BaseView.propTypes = {
|
|
34
|
+
children: PropTypes.node,
|
|
35
|
+
style: PropTypes.oneOfType([PropTypes.object, PropTypes.array])
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export default BaseView
|
package/src/utils/index.js
CHANGED