forked from primer/react
-
Notifications
You must be signed in to change notification settings - Fork 0
/
TextInput.tsx
169 lines (161 loc) · 5.42 KB
/
TextInput.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
import React, {MouseEventHandler, useCallback, useState} from 'react'
import {ForwardRefComponent as PolymorphicForwardRefComponent} from '@radix-ui/react-polymorphic'
import classnames from 'classnames'
import TextInputInnerVisualSlot from './_TextInputInnerVisualSlot'
import {useProvidedRefOrCreate} from './hooks'
import {Merge} from './utils/types'
import TextInputWrapper, {StyledWrapperProps} from './_TextInputWrapper'
import UnstyledTextInput from './_UnstyledTextInput'
import TextInputAction from './_TextInputInnerAction'
export type TextInputNonPassthroughProps = {
/** @deprecated Use `leadingVisual` or `trailingVisual` prop instead */
icon?: React.ComponentType<{className?: string}>
/** Whether the to show a loading indicator in the input */
loading?: boolean
/**
* Which position to render the loading indicator
* 'auto' (default): at the end of the input, unless a `leadingVisual` is passed. Then, it will render at the beginning
* 'leading': at the beginning of the input
* 'trailing': at the end of the input
**/
loaderPosition?: 'auto' | 'leading' | 'trailing'
/**
* A visual that renders inside the input before the typing area
*/
leadingVisual?: string | React.ComponentType<{className?: string}>
/**
* A visual that renders inside the input after the typing area
*/
trailingVisual?: string | React.ComponentType<{className?: string}>
/**
* A visual that renders inside the input after the typing area
*/
trailingAction?: React.ReactElement<React.HTMLProps<HTMLButtonElement>>
} & Pick<
StyledWrapperProps,
| 'block'
| 'contrast'
| 'disabled'
| 'monospace'
| 'sx'
| 'width'
| 'maxWidth'
| 'minWidth'
| 'variant'
| 'size'
| 'validationStatus'
>
export type TextInputProps = Merge<React.ComponentPropsWithoutRef<'input'>, TextInputNonPassthroughProps>
// using forwardRef is important so that other components (ex. SelectMenu) can autofocus the input
const TextInput = React.forwardRef<HTMLInputElement, TextInputProps>(
(
{
icon: IconComponent,
leadingVisual: LeadingVisual,
trailingVisual: TrailingVisual,
trailingAction,
block,
className,
contrast,
disabled,
loading,
loaderPosition,
monospace,
validationStatus,
sx: sxProp,
size: sizeProp,
onFocus,
onBlur,
// start deprecated props
width: widthProp,
minWidth: minWidthProp,
maxWidth: maxWidthProp,
variant: variantProp,
// end deprecated props
...inputProps
},
ref
) => {
const [isInputFocused, setIsInputFocused] = useState<boolean>(false)
const inputRef = useProvidedRefOrCreate(ref as React.RefObject<HTMLInputElement>)
// this class is necessary to style FilterSearch, plz no touchy!
const wrapperClasses = classnames(className, 'TextInput-wrapper')
const showLeadingLoadingIndicator =
loading && (loaderPosition === 'leading' || Boolean(LeadingVisual && loaderPosition !== 'trailing'))
const showTrailingLoadingIndicator =
loading && (loaderPosition === 'trailing' || Boolean(loaderPosition === 'auto' && !LeadingVisual))
const focusInput: MouseEventHandler = () => {
inputRef.current?.focus()
}
const handleInputFocus = useCallback(
e => {
setIsInputFocused(true)
onFocus && onFocus(e)
},
[onFocus]
)
const handleInputBlur = useCallback(
e => {
setIsInputFocused(false)
onBlur && onBlur(e)
},
[onBlur]
)
return (
<TextInputWrapper
block={block}
className={wrapperClasses}
validationStatus={validationStatus}
contrast={contrast}
disabled={disabled}
monospace={monospace}
sx={sxProp}
size={sizeProp}
width={widthProp}
minWidth={minWidthProp}
maxWidth={maxWidthProp}
variant={variantProp}
hasLeadingVisual={Boolean(LeadingVisual || showLeadingLoadingIndicator)}
hasTrailingVisual={Boolean(TrailingVisual || showTrailingLoadingIndicator)}
hasTrailingAction={Boolean(trailingAction)}
isInputFocused={isInputFocused}
onClick={focusInput}
aria-live="polite"
aria-busy={Boolean(loading)}
>
{IconComponent && <IconComponent className="TextInput-icon" />}
<TextInputInnerVisualSlot
visualPosition="leading"
showLoadingIndicator={showLeadingLoadingIndicator}
hasLoadingIndicator={typeof loading === 'boolean'}
>
{typeof LeadingVisual === 'function' ? <LeadingVisual /> : LeadingVisual}
</TextInputInnerVisualSlot>
<UnstyledTextInput
ref={inputRef}
disabled={disabled}
onFocus={handleInputFocus}
onBlur={handleInputBlur}
{...inputProps}
data-component="input"
/>
<TextInputInnerVisualSlot
visualPosition="trailing"
showLoadingIndicator={showTrailingLoadingIndicator}
hasLoadingIndicator={typeof loading === 'boolean'}
>
{typeof TrailingVisual === 'function' ? <TrailingVisual /> : TrailingVisual}
</TextInputInnerVisualSlot>
{trailingAction}
</TextInputWrapper>
)
}
) as PolymorphicForwardRefComponent<'input', TextInputProps>
TextInput.defaultProps = {
type: 'text',
loaderPosition: 'auto'
}
TextInput.displayName = 'TextInput'
export default Object.assign(TextInput, {
Action: TextInputAction
})