1- import React , { FocusEventHandler , KeyboardEventHandler , RefObject , useRef , useState } from 'react'
1+ import React , { FocusEventHandler , KeyboardEventHandler , MouseEventHandler , RefObject , useRef , useState } from 'react'
22import { omit } from '@styled-system/props'
33import { FocusKeys } from './behaviors/focusZone'
44import { useCombinedRefs } from './hooks/useCombinedRefs'
@@ -11,6 +11,7 @@ import {useProvidedRefOrCreate} from './hooks'
1111import UnstyledTextInput from './_UnstyledTextInput'
1212import TextInputWrapper from './_TextInputWrapper'
1313import Box from './Box'
14+ import Text from './Text'
1415import { isFocusable } from './utils/iterateFocusableElements'
1516
1617// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -48,8 +49,19 @@ type TextInputWithTokensInternalProps<TokenComponentType extends AnyReactCompone
4849 * Whether the remove buttons should be rendered in the tokens
4950 */
5051 hideTokenRemoveButtons ?: boolean
52+ /**
53+ * The number of tokens to display before truncating
54+ */
55+ visibleTokenCount ?: number
5156} & TextInputProps
5257
58+ const overflowCountFontSizeMap : Record < TokenSizeKeys , number > = {
59+ small : 0 ,
60+ medium : 1 ,
61+ large : 1 ,
62+ extralarge : 2
63+ }
64+
5365// using forwardRef is important so that other components (ex. Autocomplete) can use the ref
5466function TextInputWithTokensInnerComponent < TokenComponentType extends AnyReactComponent > (
5567 {
@@ -71,18 +83,20 @@ function TextInputWithTokensInnerComponent<TokenComponentType extends AnyReactCo
7183 minWidth : minWidthProp ,
7284 maxWidth : maxWidthProp ,
7385 variant : variantProp ,
86+ visibleTokenCount,
7487 ...rest
7588 } : TextInputWithTokensInternalProps < TokenComponentType > & {
7689 selectedTokenIndex : number | undefined
7790 setSelectedTokenIndex : React . Dispatch < React . SetStateAction < number | undefined > >
7891 } ,
7992 externalRef : React . ForwardedRef < HTMLInputElement >
8093) {
81- const { onFocus, onKeyDown, ...inputPropsRest } = omit ( rest )
94+ const { onBlur , onFocus, onKeyDown, ...inputPropsRest } = omit ( rest )
8295 const ref = useProvidedRefOrCreate < HTMLInputElement > ( externalRef as React . RefObject < HTMLInputElement > )
8396 const localInputRef = useRef < HTMLInputElement > ( null )
8497 const combinedInputRef = useCombinedRefs ( localInputRef , ref )
8598 const [ selectedTokenIndex , setSelectedTokenIndex ] = useState < number | undefined > ( )
99+ const [ tokensAreTruncated , setTokensAreTruncated ] = useState < boolean > ( Boolean ( visibleTokenCount ) )
86100 const { containerRef} = useFocusZone (
87101 {
88102 focusOutBehavior : 'wrap' ,
@@ -144,18 +158,42 @@ function TextInputWithTokensInnerComponent<TokenComponentType extends AnyReactCo
144158
145159 const handleTokenBlur : FocusEventHandler = ( ) => {
146160 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 )
147170 }
148171
149- const handleTokenKeyUp : KeyboardEventHandler = e => {
150- if ( e . key === 'Escape' ) {
172+ const handleTokenKeyUp : KeyboardEventHandler = event => {
173+ if ( event . key === 'Escape' ) {
151174 ref . current ?. focus ( )
152175 }
153176 }
154177
155- const handleInputFocus : FocusEventHandler = e => {
156- onFocus && onFocus ( e )
178+ const handleInputFocus : FocusEventHandler = event => {
179+ onFocus && onFocus ( event )
157180 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 )
158195 }
196+
159197 const handleInputKeyDown : KeyboardEventHandler = e => {
160198 if ( onKeyDown ) {
161199 onKeyDown ( e )
@@ -187,6 +225,16 @@ function TextInputWithTokensInnerComponent<TokenComponentType extends AnyReactCo
187225 }
188226 }
189227
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+
190238 return (
191239 < TextInputWrapper
192240 block = { block }
@@ -199,6 +247,7 @@ function TextInputWithTokensInnerComponent<TokenComponentType extends AnyReactCo
199247 minWidth = { minWidthProp }
200248 maxWidth = { maxWidthProp }
201249 variant = { variantProp }
250+ onClick = { focusInput }
202251 sx = { {
203252 ...( block
204253 ? {
@@ -251,19 +300,21 @@ function TextInputWithTokensInnerComponent<TokenComponentType extends AnyReactCo
251300 ref = { combinedInputRef }
252301 disabled = { disabled }
253302 onFocus = { handleInputFocus }
303+ onBlur = { handleInputBlur }
254304 onKeyDown = { handleInputKeyDown }
255305 type = "text"
256306 sx = { { height : '100%' } }
257307 { ...inputPropsRest }
258308 />
259309 </ Box >
260- { tokens . length && TokenComponent
261- ? tokens . map ( ( { id, ...tokenRest } , i ) => (
310+ { TokenComponent
311+ ? visibleTokens . map ( ( { id, ...tokenRest } , i ) => (
262312 < TokenComponent
263313 key = { id }
264314 onFocus = { handleTokenFocus ( i ) }
265315 onBlur = { handleTokenBlur }
266316 onKeyUp = { handleTokenKeyUp }
317+ onClick = { preventTokenClickPropagation }
267318 isSelected = { selectedTokenIndex === i }
268319 onRemove = { ( ) => {
269320 handleTokenRemove ( id )
@@ -275,6 +326,11 @@ function TextInputWithTokensInnerComponent<TokenComponentType extends AnyReactCo
275326 />
276327 ) )
277328 : null }
329+ { tokensAreTruncated ? (
330+ < Text color = "fg.muted" fontSize = { size && overflowCountFontSizeMap [ size ] } >
331+ +{ tokens . length - visibleTokens . length }
332+ </ Text >
333+ ) : null }
278334 </ Box >
279335 </ TextInputWrapper >
280336 )
0 commit comments