@@ -14,12 +14,13 @@ See the License for the specific language governing permissions and
14
14
limitations under the License.
15
15
*/
16
16
17
- import React , { InputHTMLAttributes , SelectHTMLAttributes , TextareaHTMLAttributes } from 'react' ;
17
+ import React , { InputHTMLAttributes , SelectHTMLAttributes , TextareaHTMLAttributes , RefObject } from 'react' ;
18
18
import classNames from 'classnames' ;
19
19
import { debounce } from "lodash" ;
20
20
21
21
import * as sdk from '../../../index' ;
22
22
import { IFieldState , IValidationResult } from "./Validation" ;
23
+ import { ComponentClass } from "../../../@types/common" ;
23
24
24
25
// Invoke validation from user input (when typing, etc.) at most once every N ms.
25
26
const VALIDATION_THROTTLE_MS = 200 ;
@@ -78,26 +79,45 @@ interface IProps {
78
79
}
79
80
80
81
export interface IInputProps extends IProps , InputHTMLAttributes < HTMLInputElement > {
82
+ // The ref pass through to the input
83
+ inputRef ?: RefObject < HTMLInputElement > ;
81
84
// The element to create. Defaults to "input".
82
85
element ?: "input" ;
86
+ componentClass ?: undefined ;
83
87
// The input's value. This is a controlled component, so the value is required.
84
88
value : string ;
85
89
}
86
90
87
91
interface ISelectProps extends IProps , SelectHTMLAttributes < HTMLSelectElement > {
92
+ // The ref pass through to the select
93
+ inputRef ?: RefObject < HTMLSelectElement > ;
88
94
// To define options for a select, use <Field><option ... /></Field>
89
95
element : "select" ;
96
+ componentClass ?: undefined ;
90
97
// The select's value. This is a controlled component, so the value is required.
91
98
value : string ;
92
99
}
93
100
94
101
interface ITextareaProps extends IProps , TextareaHTMLAttributes < HTMLTextAreaElement > {
102
+ // The ref pass through to the textarea
103
+ inputRef ?: RefObject < HTMLTextAreaElement > ;
95
104
element : "textarea" ;
105
+ componentClass ?: undefined ;
96
106
// The textarea's value. This is a controlled component, so the value is required.
97
107
value : string ;
98
108
}
99
109
100
- type PropShapes = IInputProps | ISelectProps | ITextareaProps ;
110
+ export interface INativeOnChangeInputProps extends IProps , InputHTMLAttributes < HTMLInputElement > {
111
+ // The ref pass through to the input
112
+ inputRef ?: RefObject < HTMLInputElement > ;
113
+ element : "input" ;
114
+ // The custom component to render
115
+ componentClass : ComponentClass ;
116
+ // The input's value. This is a controlled component, so the value is required.
117
+ value : string ;
118
+ }
119
+
120
+ type PropShapes = IInputProps | ISelectProps | ITextareaProps | INativeOnChangeInputProps ;
101
121
102
122
interface IState {
103
123
valid : boolean ;
@@ -108,7 +128,7 @@ interface IState {
108
128
109
129
export default class Field extends React . PureComponent < PropShapes , IState > {
110
130
private id : string ;
111
- private input : HTMLInputElement ;
131
+ private inputRef : RefObject < HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement > ;
112
132
113
133
public static readonly defaultProps = {
114
134
element : "input" ,
@@ -146,7 +166,7 @@ export default class Field extends React.PureComponent<PropShapes, IState> {
146
166
}
147
167
148
168
public focus ( ) {
149
- this . input . focus ( ) ;
169
+ this . inputRef . current ? .focus ( ) ;
150
170
// programmatic does not fire onFocus handler
151
171
this . setState ( {
152
172
focused : true ,
@@ -197,7 +217,7 @@ export default class Field extends React.PureComponent<PropShapes, IState> {
197
217
if ( ! this . props . onValidate ) {
198
218
return ;
199
219
}
200
- const value = this . input ? this . input . value : null ;
220
+ const value = this . inputRef . current ?. value ?? null ;
201
221
const { valid, feedback } = await this . props . onValidate ( {
202
222
value,
203
223
focused,
@@ -228,13 +248,13 @@ export default class Field extends React.PureComponent<PropShapes, IState> {
228
248
229
249
public render ( ) {
230
250
/* eslint @typescript-eslint/no-unused-vars: ["error", { "ignoreRestSiblings": true }] */
231
- const { element, prefixComponent, postfixComponent, className, onValidate, children,
251
+ const { element, componentClass , inputRef , prefixComponent, postfixComponent, className, onValidate, children,
232
252
tooltipContent, forceValidity, tooltipClassName, list, validateOnBlur, validateOnChange, validateOnFocus,
233
253
usePlaceholderAsHint, forceTooltipVisible,
234
254
...inputProps } = this . props ;
235
255
236
- // Set some defaults for the <input> element
237
- const ref = input => this . input = input ;
256
+ this . inputRef = inputRef || React . createRef ( ) ;
257
+
238
258
inputProps . placeholder = inputProps . placeholder || inputProps . label ;
239
259
inputProps . id = this . id ; // this overwrites the id from props
240
260
@@ -243,9 +263,9 @@ export default class Field extends React.PureComponent<PropShapes, IState> {
243
263
inputProps . onBlur = this . onBlur ;
244
264
245
265
// Appease typescript's inference
246
- const inputProps_ = { ...inputProps , ref, list } ;
266
+ const inputProps_ = { ...inputProps , ref : this . inputRef , list } ;
247
267
248
- const fieldInput = React . createElement ( this . props . element , inputProps_ , children ) ;
268
+ const fieldInput = React . createElement ( this . props . componentClass || this . props . element , inputProps_ , children ) ;
249
269
250
270
let prefixContainer = null ;
251
271
if ( prefixComponent ) {
@@ -257,17 +277,22 @@ export default class Field extends React.PureComponent<PropShapes, IState> {
257
277
}
258
278
259
279
const hasValidationFlag = forceValidity !== null && forceValidity !== undefined ;
260
- const fieldClasses = classNames ( "mx_Field" , `mx_Field_${ this . props . element } ` , className , {
261
- // If we have a prefix element, leave the label always at the top left and
262
- // don't animate it, as it looks a bit clunky and would add complexity to do
263
- // properly.
264
- mx_Field_labelAlwaysTopLeft : prefixComponent || usePlaceholderAsHint ,
265
- mx_Field_placeholderIsHint : usePlaceholderAsHint ,
266
- mx_Field_valid : hasValidationFlag ? forceValidity : onValidate && this . state . valid === true ,
267
- mx_Field_invalid : hasValidationFlag
268
- ? ! forceValidity
269
- : onValidate && this . state . valid === false ,
270
- } ) ;
280
+ const fieldClasses = classNames (
281
+ "mx_Field" ,
282
+ `mx_Field_${ this . props . element } ` ,
283
+ className ,
284
+ {
285
+ // If we have a prefix element, leave the label always at the top left and
286
+ // don't animate it, as it looks a bit clunky and would add complexity to do
287
+ // properly.
288
+ mx_Field_labelAlwaysTopLeft : prefixComponent || usePlaceholderAsHint ,
289
+ mx_Field_placeholderIsHint : usePlaceholderAsHint ,
290
+ mx_Field_valid : hasValidationFlag ? forceValidity : onValidate && this . state . valid === true ,
291
+ mx_Field_invalid : hasValidationFlag
292
+ ? ! forceValidity
293
+ : onValidate && this . state . valid === false ,
294
+ } ,
295
+ ) ;
271
296
272
297
// Handle displaying feedback on validity
273
298
// FIXME: Using an import will result in test failures
0 commit comments