1
- import React , { FocusEventHandler , KeyboardEventHandler , RefObject , useRef , useState } from 'react'
1
+ import React , { FocusEventHandler , KeyboardEventHandler , MouseEventHandler , RefObject , useRef , useState } from 'react'
2
2
import { omit } from '@styled-system/props'
3
3
import { FocusKeys } from './behaviors/focusZone'
4
4
import { useCombinedRefs } from './hooks/useCombinedRefs'
@@ -11,6 +11,7 @@ import {useProvidedRefOrCreate} from './hooks'
11
11
import UnstyledTextInput from './_UnstyledTextInput'
12
12
import TextInputWrapper from './_TextInputWrapper'
13
13
import Box from './Box'
14
+ import Text from './Text'
14
15
import { isFocusable } from './utils/iterateFocusableElements'
15
16
16
17
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -48,8 +49,19 @@ type TextInputWithTokensInternalProps<TokenComponentType extends AnyReactCompone
48
49
* Whether the remove buttons should be rendered in the tokens
49
50
*/
50
51
hideTokenRemoveButtons ?: boolean
52
+ /**
53
+ * The number of tokens to display before truncating
54
+ */
55
+ visibleTokenCount ?: number
51
56
} & TextInputProps
52
57
58
+ const overflowCountFontSizeMap : Record < TokenSizeKeys , number > = {
59
+ small : 0 ,
60
+ medium : 1 ,
61
+ large : 1 ,
62
+ extralarge : 2
63
+ }
64
+
53
65
// using forwardRef is important so that other components (ex. Autocomplete) can use the ref
54
66
function TextInputWithTokensInnerComponent < TokenComponentType extends AnyReactComponent > (
55
67
{
@@ -71,18 +83,20 @@ function TextInputWithTokensInnerComponent<TokenComponentType extends AnyReactCo
71
83
minWidth : minWidthProp ,
72
84
maxWidth : maxWidthProp ,
73
85
variant : variantProp ,
86
+ visibleTokenCount,
74
87
...rest
75
88
} : TextInputWithTokensInternalProps < TokenComponentType > & {
76
89
selectedTokenIndex : number | undefined
77
90
setSelectedTokenIndex : React . Dispatch < React . SetStateAction < number | undefined > >
78
91
} ,
79
92
externalRef : React . ForwardedRef < HTMLInputElement >
80
93
) {
81
- const { onFocus, onKeyDown, ...inputPropsRest } = omit ( rest )
94
+ const { onBlur , onFocus, onKeyDown, ...inputPropsRest } = omit ( rest )
82
95
const ref = useProvidedRefOrCreate < HTMLInputElement > ( externalRef as React . RefObject < HTMLInputElement > )
83
96
const localInputRef = useRef < HTMLInputElement > ( null )
84
97
const combinedInputRef = useCombinedRefs ( localInputRef , ref )
85
98
const [ selectedTokenIndex , setSelectedTokenIndex ] = useState < number | undefined > ( )
99
+ const [ tokensAreTruncated , setTokensAreTruncated ] = useState < boolean > ( Boolean ( visibleTokenCount ) )
86
100
const { containerRef} = useFocusZone (
87
101
{
88
102
focusOutBehavior : 'wrap' ,
@@ -144,18 +158,42 @@ function TextInputWithTokensInnerComponent<TokenComponentType extends AnyReactCo
144
158
145
159
const handleTokenBlur : FocusEventHandler = ( ) => {
146
160
setSelectedTokenIndex ( undefined )
161
+
162
+ // HACK: wait a tick and check the focused element before hiding truncated tokens
163
+ // this prevents the tokens from hiding when the user is moving focus between tokens,
164
+ // but still hides the tokens when the user blurs the token by tabbing out or clicking somewhere else on the page
165
+ setTimeout ( ( ) => {
166
+ if ( ! containerRef . current ?. contains ( document . activeElement ) && visibleTokenCount ) {
167
+ setTokensAreTruncated ( true )
168
+ }
169
+ } , 0 )
147
170
}
148
171
149
- const handleTokenKeyUp : KeyboardEventHandler = e => {
150
- if ( e . key === 'Escape' ) {
172
+ const handleTokenKeyUp : KeyboardEventHandler = event => {
173
+ if ( event . key === 'Escape' ) {
151
174
ref . current ?. focus ( )
152
175
}
153
176
}
154
177
155
- const handleInputFocus : FocusEventHandler = e => {
156
- onFocus && onFocus ( e )
178
+ const handleInputFocus : FocusEventHandler = event => {
179
+ onFocus && onFocus ( event )
157
180
setSelectedTokenIndex ( undefined )
181
+ visibleTokenCount && setTokensAreTruncated ( false )
182
+ }
183
+
184
+ const handleInputBlur : FocusEventHandler = event => {
185
+ onBlur && onBlur ( event )
186
+
187
+ // HACK: wait a tick and check the focused element before hiding truncated tokens
188
+ // this prevents the tokens from hiding when the user is moving focus from the input to a token,
189
+ // but still hides the tokens when the user blurs the input by tabbing out or clicking somewhere else on the page
190
+ setTimeout ( ( ) => {
191
+ if ( ! containerRef . current ?. contains ( document . activeElement ) && visibleTokenCount ) {
192
+ setTokensAreTruncated ( true )
193
+ }
194
+ } , 0 )
158
195
}
196
+
159
197
const handleInputKeyDown : KeyboardEventHandler = e => {
160
198
if ( onKeyDown ) {
161
199
onKeyDown ( e )
@@ -187,6 +225,16 @@ function TextInputWithTokensInnerComponent<TokenComponentType extends AnyReactCo
187
225
}
188
226
}
189
227
228
+ const focusInput : MouseEventHandler = ( ) => {
229
+ combinedInputRef . current ?. focus ( )
230
+ }
231
+
232
+ const preventTokenClickPropagation : MouseEventHandler = event => {
233
+ event . stopPropagation ( )
234
+ }
235
+
236
+ const visibleTokens = tokensAreTruncated ? tokens . slice ( 0 , visibleTokenCount ) : tokens
237
+
190
238
return (
191
239
< TextInputWrapper
192
240
block = { block }
@@ -199,6 +247,7 @@ function TextInputWithTokensInnerComponent<TokenComponentType extends AnyReactCo
199
247
minWidth = { minWidthProp }
200
248
maxWidth = { maxWidthProp }
201
249
variant = { variantProp }
250
+ onClick = { focusInput }
202
251
sx = { {
203
252
...( block
204
253
? {
@@ -251,19 +300,21 @@ function TextInputWithTokensInnerComponent<TokenComponentType extends AnyReactCo
251
300
ref = { combinedInputRef }
252
301
disabled = { disabled }
253
302
onFocus = { handleInputFocus }
303
+ onBlur = { handleInputBlur }
254
304
onKeyDown = { handleInputKeyDown }
255
305
type = "text"
256
306
sx = { { height : '100%' } }
257
307
{ ...inputPropsRest }
258
308
/>
259
309
</ Box >
260
- { tokens . length && TokenComponent
261
- ? tokens . map ( ( { id, ...tokenRest } , i ) => (
310
+ { TokenComponent
311
+ ? visibleTokens . map ( ( { id, ...tokenRest } , i ) => (
262
312
< TokenComponent
263
313
key = { id }
264
314
onFocus = { handleTokenFocus ( i ) }
265
315
onBlur = { handleTokenBlur }
266
316
onKeyUp = { handleTokenKeyUp }
317
+ onClick = { preventTokenClickPropagation }
267
318
isSelected = { selectedTokenIndex === i }
268
319
onRemove = { ( ) => {
269
320
handleTokenRemove ( id )
@@ -275,6 +326,11 @@ function TextInputWithTokensInnerComponent<TokenComponentType extends AnyReactCo
275
326
/>
276
327
) )
277
328
: null }
329
+ { tokensAreTruncated ? (
330
+ < Text color = "fg.muted" fontSize = { size && overflowCountFontSizeMap [ size ] } >
331
+ +{ tokens . length - visibleTokens . length }
332
+ </ Text >
333
+ ) : null }
278
334
</ Box >
279
335
</ TextInputWrapper >
280
336
)
0 commit comments