feat(TokenizedInput): Added new component TokenizedInput#375
feat(TokenizedInput): Added new component TokenizedInput#375feelsbadmans wants to merge 8 commits intomainfrom
Conversation
|
Preview is ready. |
korvin89
left a comment
There was a problem hiding this comment.
Review: TokenizedInput
Architecturally well-designed (composition pattern, undo/redo, platform-specific shortcuts), but the implementation needs work on reliability and testability. See inline comments below.
High: 9 issues (potential runtime crashes, memory leaks, SSR problems, stale closures)
Medium: 16 issues (missing tests for hooks, single context perf, 501-line hook, unnecessary dependency)
Low: 10 issues (accessibility, magic numbers, minor optimizations)
| ArrowDown = 'ArrowDown', | ||
| ArrowRight = 'ArrowRight', | ||
| PageUp = 'PageUp', | ||
| PageDown = 'PageDown', |
There was a problem hiding this comment.
Consider replacing TypeScript enum with as const object. Enums are not tree-shakeable, generate IIFEs in JS output, and can cause issues with isolatedModules.
export const KeyCode = {
Enter: 'Enter',
Backspace: 'Backspace',
Escape: 'Escape',
ArrowLeft: 'ArrowLeft',
ArrowRight: 'ArrowRight',
ArrowUp: 'ArrowUp',
ArrowDown: 'ArrowDown',
Tab: 'Tab',
} as const;
export type KeyCode = (typeof KeyCode)[keyof typeof KeyCode];| } | ||
| return navigator.userAgent.toUpperCase().indexOf('MAC') >= 0; | ||
| }; | ||
|
|
There was a problem hiding this comment.
isMac() is called at module level — in SSR environments window is undefined, so the result is always false (Win shortcuts). If the module is cached on the server and reused on the client, shortcuts will be wrong.
Also, navigator.userAgent parsing is a legacy approach. Consider navigator.userAgentData?.platform or at least lazy initialization inside the hook:
const useShortcuts = () => {
return React.useMemo(() => {
if (typeof window === 'undefined') return winShortcuts;
return navigator.userAgent.toUpperCase().includes('MAC')
? macShortcuts
: winShortcuts;
}, []);
};| undoRedo, | ||
| ], | ||
| ); | ||
| }; |
There was a problem hiding this comment.
501 lines in a single hook is a lot. Each sub-handler (navigationHandler, deleteHandler, blurHandler, undoRedo) could be extracted into its own hook for better readability and testability.
|
|
||
| if ( | ||
| (focus.key === key && focus.idx === idx) || | ||
| tokens[idx].options?.readOnlyFields?.includes(key) |
There was a problem hiding this comment.
Potential crash: tokens[idx] can be undefined if prevField returned an invalid idx.
tokens[idx].options?.readOnlyFields?.includes(key)Add a guard:
const token = tokens[idx];
if (!token) return false;| value: {...t.value, ...newValue}, | ||
| errors: undefined, | ||
| } | ||
| : {...t}, |
There was a problem hiding this comment.
onRemoveToken has no bounds check on idx. If idx < 0 or idx >= tokens.length, filter silently returns the array unchanged, but undoRedoManager records an invalid operation.
if (idx < 0 || idx >= tokens.length) return;| shouldAllowBlur: () => true, | ||
| }, | ||
| }); | ||
|
|
There was a problem hiding this comment.
A single context holds inputInfo, focusInfo, and options. Any change in any of them triggers a re-render of all consumers. For a component with frequent updates (every keystroke), this can be a performance problem.
Consider splitting into 2-3 contexts (input, focus, options) so consumers only re-render when their specific slice changes.
|
|
||
| const token = tokens[idx]; | ||
|
|
||
| const hasChanges = React.useRef(false); |
There was a problem hiding this comment.
hasChanges ref is not reset when idx changes. If React reuses the component for a different token (on delete/insert), the ref may remain true from the previous one:
React.useEffect(() => {
hasChanges.current = false;
}, [idx]);| onClick={onClear} | ||
| tabIndex={-1} | ||
| aria-label="tokenized-input-clear-button" | ||
| > |
There was a problem hiding this comment.
aria-label="tokenized-input-clear-button" is a technical identifier, not a user-facing label. For accessibility:
aria-label={i18n('clear-input')}Also, <Xmark /> is missing a title prop — gravity-ui guidelines recommend adding title to Icon components.
| } from './components'; | ||
|
|
||
| export type TokenValueBase = Record<string, string>; | ||
|
|
There was a problem hiding this comment.
isNew is runtime state, not token data. Mixing runtime flags with data in one type blurs separation of concerns. Consider a discriminated union:
type Token<T> =
| { kind: 'regular'; id: string; value: T; errors?: ...; options?: ... }
| { kind: 'new'; id: string; value: T };| - `Escape` — close the suggestions menu; press again to remove focus | ||
| - `Cmd + I` (Mac) / `Ctrl + I` (Win/Linux) — open the suggestions menu | ||
| - `Cmd + Enter` (Mac) / `Ctrl + Enter` (Win/Linux) — finish the current token and go to the next (when the suggestions menu is closed) | ||
| - `Enter` — select a suggestion / finish the current token and go to the next (when the suggestions menu is closed) |
There was a problem hiding this comment.
For a 5500-line component, the README is quite brief. Missing:
- API reference (props table)
- Usage examples (code snippets)
- Composition pattern description — how to override sub-components
- Mac vs Win shortcuts listed separately
6afcc91 to
6590c92
Compare
No description provided.