|
| 1 | +## TokenizedInput |
| 2 | + |
| 3 | +This component is for writing queries/filters and working with them as tokens. Here, a token is an expression (for example, for the format `key = value` the token would be `User = Ivan`). A distinguishing feature is full keyboard and mouse support (including clicking suggestions). |
| 4 | + |
| 5 | +### API Reference |
| 6 | + |
| 7 | +| Prop | Type | Default | Description | |
| 8 | +| :---------------------- | :------------------------------------------------------------------------------------------------------------------- | :-------------- | :------------------------------------------------------------- | |
| 9 | +| `tokens` | `T[]` | - | Array of token values. | |
| 10 | +| `fields` | `TokenizedInputTokenField<T>[]` | - | Field definitions; order matches display order. | |
| 11 | +| `onChange` | `(newTokens: T[]) => void` | - | Token list change handler. | |
| 12 | +| `defaultTokens` | `T[]` | `[]` | Defaults applied on full clear. | |
| 13 | +| `transformTokens` | `(tokens: T[]) => TokenizedInputToken<T>[]` | - | Maps raw tokens to internal token shape. | |
| 14 | +| `validateToken` | `(token: T) => Partial<Record<keyof T, string>> \| undefined \| false` | - | Validates a token. | |
| 15 | +| `formatToken` | `(token: T) => T` | - | Formats a token value before saving. | |
| 16 | +| `placeholder` | `string \| TokenizedInputTokenPlaceholderGeneratorFn<T>` | - | Placeholder for the new token. | |
| 17 | +| `isEditable` | `boolean` | `true` | Whether editing is allowed. | |
| 18 | +| `isClearable` | `boolean` | `true` | Whether full clear is allowed. | |
| 19 | +| `debounceDelay` | `number \| Record<keyof T, number>` | `150` | Suggestions debounce delay; per-field overrides are supported. | |
| 20 | +| `debounceFlushStrategy` | `'focus-input' \| 'focus-field'` | `'focus-field'` | When debounce flushes. | |
| 21 | +| `autoFocus` | `boolean` | `false` | Autofocus the new token. | |
| 22 | +| `onSuggest` | `(ctx: TokenizedInputSuggestionContext<T>) => TokenizedInputSuggestions<T> \| Promise<TokenizedInputSuggestions<T>>` | - | Fetches suggestions. | |
| 23 | +| `filterSuggestions` | `(items: TokenizedInputSuggestionsItem<T>[], search: string) => TokenizedInputSuggestionsItem<T>[]` | - | Custom function to filter suggestions based on search string. | |
| 24 | +| `fullWidthSuggestions` | `boolean` | `false` | Render suggestions full width below the input. | |
| 25 | +| `onKeyDown` | `(v: TokenizedInputTokenOnKeyDownOptions<T>) => boolean` | - | Keydown handler; return true to stop further handling. | |
| 26 | +| `onFocus` | `() => void` | - | onFocus callback. | |
| 27 | +| `onBlur` | `() => void` | - | onBlur callback. | |
| 28 | +| `shouldAllowBlur` | `(e: React.FocusEvent) => boolean` | `() => true` | Return true to allow blur, false to prevent it. | |
| 29 | + |
| 30 | +### Usage Examples |
| 31 | + |
| 32 | +#### 1. Basic Key-Value Input |
| 33 | + |
| 34 | +The most common use case is a query builder where each token consists of a `key`, `operator`, and `value`. |
| 35 | + |
| 36 | +```tsx |
| 37 | +import {TokenizedInput} from '@gravity-ui/components'; |
| 38 | + |
| 39 | +type MyToken = { |
| 40 | + key: string; |
| 41 | + operator: string; |
| 42 | + value: string; |
| 43 | +}; |
| 44 | + |
| 45 | +const fields = [ |
| 46 | + {key: 'key', className: 'my-key-field'}, |
| 47 | + {key: 'operator', className: 'my-operator-field'}, |
| 48 | + {key: 'value', className: 'my-value-field'}, |
| 49 | +]; |
| 50 | + |
| 51 | +function App() { |
| 52 | + const [tokens, setTokens] = React.useState<MyToken[]>([]); |
| 53 | + |
| 54 | + return ( |
| 55 | + <TokenizedInput<MyToken> |
| 56 | + tokens={tokens} |
| 57 | + fields={fields} |
| 58 | + onChange={setTokens} |
| 59 | + onSuggest={async ({key, value}) => { |
| 60 | + // Return suggestions based on the current field |
| 61 | + if (key === 'key') { |
| 62 | + return { |
| 63 | + items: [ |
| 64 | + { |
| 65 | + label: 'Status', |
| 66 | + search: 'Status', |
| 67 | + value: {key: 'Status'}, |
| 68 | + focus: {idx: 0, key: 'operator', offset: -1}, |
| 69 | + }, |
| 70 | + { |
| 71 | + label: 'User', |
| 72 | + search: 'User', |
| 73 | + value: {key: 'User'}, |
| 74 | + focus: {idx: 0, key: 'operator', offset: -1}, |
| 75 | + }, |
| 76 | + ], |
| 77 | + }; |
| 78 | + } |
| 79 | + return {items: []}; |
| 80 | + }} |
| 81 | + /> |
| 82 | + ); |
| 83 | +} |
| 84 | +``` |
| 85 | + |
| 86 | +#### 2. Single Field (Tags) Input |
| 87 | + |
| 88 | +You can use `TokenizedInput` as a simple tags input by defining only one field and using `transformTokens` to make existing tokens read-only. |
| 89 | + |
| 90 | +```tsx |
| 91 | +import {getUniqId} from '@gravity-ui/uikit'; |
| 92 | +import {TokenizedInput, TokenizedInputToken} from '@gravity-ui/components'; |
| 93 | + |
| 94 | +type TagToken = {value: string}; |
| 95 | + |
| 96 | +const fields = [ |
| 97 | + { |
| 98 | + key: 'value', |
| 99 | + specialKeysActions: [ |
| 100 | + { |
| 101 | + // Create a new token when pressing Space |
| 102 | + key: (e) => e.key === ' ', |
| 103 | + action: ({focus, onFocus, event}) => { |
| 104 | + event.preventDefault(); |
| 105 | + onFocus({...focus, idx: focus.idx + 1, key: 'value', offset: -1}); |
| 106 | + }, |
| 107 | + }, |
| 108 | + ], |
| 109 | + }, |
| 110 | +]; |
| 111 | + |
| 112 | +// Make existing tokens read-only so they act like solid blocks (tags) |
| 113 | +const transformTokens = (tokens: TagToken[]): TokenizedInputToken<TagToken>[] => { |
| 114 | + return tokens.map((t) => ({ |
| 115 | + id: getUniqId(), |
| 116 | + value: t, |
| 117 | + kind: 'regular', |
| 118 | + options: {readOnlyFields: ['value']}, // Prevents editing the text inside the tag |
| 119 | + })); |
| 120 | +}; |
| 121 | + |
| 122 | +function TagsInput() { |
| 123 | + const [tokens, setTokens] = React.useState<TagToken[]>([]); |
| 124 | + |
| 125 | + return ( |
| 126 | + <TokenizedInput |
| 127 | + tokens={tokens} |
| 128 | + transformTokens={transformTokens} |
| 129 | + onChange={setTokens} |
| 130 | + fields={fields} |
| 131 | + /> |
| 132 | + ); |
| 133 | +} |
| 134 | +``` |
| 135 | + |
| 136 | +#### 3. Dynamic Placeholders |
| 137 | + |
| 138 | +You can provide a function to the `placeholder` prop to generate context-aware placeholders based on the token's current state. |
| 139 | + |
| 140 | +```tsx |
| 141 | +const placeholder = React.useCallback((tokenType, token, idx) => { |
| 142 | + // Show a specific placeholder when the user is about to type a value for the "message" key |
| 143 | + if (token.key === 'message' && idx === 2) { |
| 144 | + return 'Enter a string'; |
| 145 | + } |
| 146 | + // Show a general placeholder for the very first empty token |
| 147 | + if (tokenType === 'new' && idx === 0) { |
| 148 | + return 'Enter a value'; |
| 149 | + } |
| 150 | + return undefined; |
| 151 | +}, []); |
| 152 | + |
| 153 | +<TokenizedInput placeholder={placeholder} /* ... */ />; |
| 154 | +``` |
| 155 | + |
| 156 | +### Composition Pattern |
| 157 | + |
| 158 | +The `TokenizedInput` component is highly modular and built using a composition of smaller sub-components. You can override any of its sub-components by passing a custom component directly to the corresponding prop. |
| 159 | + |
| 160 | +The available sub-components are: |
| 161 | + |
| 162 | +- `Wrapper`: The outermost container that handles global key presses and blur events. |
| 163 | +- `TokenList`: Renders the list of tokens. |
| 164 | +- `Token`: Renders an individual token (either a `RegularToken` or a `NewToken`). |
| 165 | +- `Field`: Renders the actual input field inside a token. |
| 166 | +- `Suggestions`: Renders the suggestions popup. |
| 167 | + |
| 168 | +#### Overriding Sub-components |
| 169 | + |
| 170 | +If you want to customize the rendering of the input values (e.g., to add syntax highlighting or custom formatting), you can override the `Field` component to use the `renderValue` prop. |
| 171 | + |
| 172 | +```tsx |
| 173 | +import {TokenizedInput, TokenizedInputFieldProps} from '@gravity-ui/components'; |
| 174 | + |
| 175 | +// 1. Define your custom render logic |
| 176 | +const renderValue: TokenizedInputFieldProps['renderValue'] = ({ |
| 177 | + fieldKey, |
| 178 | + isFocused, |
| 179 | + isNew, |
| 180 | + visibleValue, |
| 181 | +}) => { |
| 182 | + // Don't format while typing |
| 183 | + if (isNew || isFocused) { |
| 184 | + return visibleValue; |
| 185 | + } |
| 186 | + |
| 187 | + // Apply custom formatting based on the field type |
| 188 | + if (fieldKey === 'key') { |
| 189 | + return <span style={{color: 'purple', fontWeight: 800}}>{visibleValue}</span>; |
| 190 | + } |
| 191 | + if (fieldKey === 'value') { |
| 192 | + return <span style={{color: 'green'}}>{visibleValue}</span>; |
| 193 | + } |
| 194 | + |
| 195 | + return visibleValue; |
| 196 | +}; |
| 197 | + |
| 198 | +// 2. Create a custom Field component that wraps the original Field |
| 199 | +const CustomField = (props: TokenizedInputFieldProps) => { |
| 200 | + return <TokenizedInput.Field {...props} renderValue={renderValue} />; |
| 201 | +}; |
| 202 | + |
| 203 | +// 3. Pass it to the Field prop |
| 204 | +function App() { |
| 205 | + return ( |
| 206 | + <TokenizedInput |
| 207 | + /* ...other props... */ |
| 208 | + Field={CustomField} |
| 209 | + /> |
| 210 | + ); |
| 211 | +} |
| 212 | +``` |
| 213 | + |
| 214 | +#### Context Hooks |
| 215 | + |
| 216 | +When building fully custom sub-components, you can use the provided context hooks to access the internal state and callbacks of the `TokenizedInput`: |
| 217 | + |
| 218 | +- `useTokenizedInputContext()` — Input state and callbacks (`tokens`, `fields`, `onChangeToken`, `onRemoveToken`, etc.) |
| 219 | +- `useTokenizedInputFocusContext()` — Focus state and callbacks (`focus`, `onFocus`, `onBlur`, `getFocusRules`, etc.) |
| 220 | +- `useTokenizedInputOptionsContext()` — Extra options from props (`onSuggest`, `debounceDelay`, `shouldAllowBlur`, etc.) |
| 221 | +- `useTokenizedInput()` — Returns all three contexts above combined (`inputInfo`, `focusInfo`, `options`). |
| 222 | +- `useTokenizedInputComponents()` — Access to the current sub-components (useful if your custom component needs to render the default `Field` or `Token`). |
| 223 | + |
| 224 | +For convenience, there are also specific hooks for each part of the component that you can use as a starting point: `useTokenizedInputWrapper`, `useTokenizedInputList`, `useTokenizedInputNewToken`, `useTokenizedInputRegularToken`, `useTokenizedInputField`, `useTokenizedInputSuggestions`. |
| 225 | + |
| 226 | +### Hotkeys |
| 227 | + |
| 228 | +#### Mac |
| 229 | + |
| 230 | +- `Cmd + Arrow` — move between tokens |
| 231 | +- `Option + Arrow` — move between token fields |
| 232 | +- `Cmd + Backspace` — delete the current token |
| 233 | +- `Cmd + Z` — undo |
| 234 | +- `Cmd + Shift + Z` — redo |
| 235 | +- `Cmd + I` — open the suggestions menu |
| 236 | +- `Cmd + Enter` — finish the current token and go to the next (when the suggestions menu is closed) |
| 237 | + |
| 238 | +#### Windows / Linux |
| 239 | + |
| 240 | +- `Ctrl + Alt + Arrow` — move between tokens |
| 241 | +- `Ctrl + Arrow` — move between token fields |
| 242 | +- `Ctrl + Alt + Backspace` — delete the current token |
| 243 | +- `Ctrl + Z` — undo |
| 244 | +- `Ctrl + Y` or `Ctrl + Shift + Z` — redo |
| 245 | +- `Ctrl + I` — open the suggestions menu |
| 246 | +- `Ctrl + Enter` — finish the current token and go to the next (when the suggestions menu is closed) |
| 247 | + |
| 248 | +#### General |
| 249 | + |
| 250 | +- `Escape` — close the suggestions menu; press again to remove focus |
| 251 | +- `Enter` — select a suggestion / finish the current token and go to the next (when the suggestions menu is closed) |
0 commit comments