diff --git a/packages/cli-kit/src/private/node/ui/components/AutocompletePrompt.test.tsx b/packages/cli-kit/src/private/node/ui/components/AutocompletePrompt.test.tsx index 6f1f390a53..8e04bb76d7 100644 --- a/packages/cli-kit/src/private/node/ui/components/AutocompletePrompt.test.tsx +++ b/packages/cli-kit/src/private/node/ui/components/AutocompletePrompt.test.tsx @@ -448,7 +448,7 @@ describe('AutocompletePrompt', async () => { " `) - await sendInputAndWaitForChange(renderInstance, DELETE) + await sendInputAndWaitForContent(renderInstance, 'ype to search', DELETE) expect(renderInstance.lastFrame()).toMatchInlineSnapshot(` "? Associate your project with the org Castile Ventures? Type to search... @@ -697,7 +697,7 @@ describe('AutocompletePrompt', async () => { " `) - await sendInputAndWaitForChange(renderInstance, DELETE) + await sendInputAndWaitForContent(renderInstance, 'ype to search', DELETE) expect(renderInstance.lastFrame()).toMatchInlineSnapshot(` "? Associate your project with the org Castile Ventures? Type to search... diff --git a/packages/cli-kit/src/private/node/ui/components/TextInput.tsx b/packages/cli-kit/src/private/node/ui/components/TextInput.tsx index 95cee6c85d..fa74d41c75 100644 --- a/packages/cli-kit/src/private/node/ui/components/TextInput.tsx +++ b/packages/cli-kit/src/private/node/ui/components/TextInput.tsx @@ -1,6 +1,6 @@ /* eslint-disable no-nested-ternary */ import {shouldDisplayColors} from '../../../../public/node/output.js' -import React, {useEffect, useState} from 'react' +import React, {useLayoutEffect, useState} from 'react' import {Text, useInput} from 'ink' import chalk from 'chalk' import figures from 'figures' @@ -29,18 +29,13 @@ const TextInput: FunctionComponent = ({ }: TextInputProps) => { const [cursorOffset, setCursorOffset] = useState((originalValue || '').length) - // if the updated value is shorter than the last one we need to reset the cursor - useEffect(() => { - setCursorOffset((previousOffset) => { - const newValue = originalValue || '' + const clampedCursorOffset = Math.min(cursorOffset, (originalValue || '').length) - if (previousOffset > newValue.length - 1) { - return newValue.length - } - - return previousOffset - }) - }, [originalValue]) + useLayoutEffect(() => { + if (clampedCursorOffset !== cursorOffset) { + setCursorOffset(clampedCursorOffset) + } + }, [clampedCursorOffset, cursorOffset]) const value = password ? '*'.repeat(originalValue.length) : originalValue let renderedValue @@ -60,7 +55,7 @@ const TextInput: FunctionComponent = ({ renderedValue = value .split('') .map((char, index) => { - if (index === cursorOffset) { + if (index === clampedCursorOffset) { return noColor ? cursorChar : chalk.inverse(char) } else { return char @@ -68,7 +63,7 @@ const TextInput: FunctionComponent = ({ }) .join('') - if (cursorOffset === value.length) { + if (clampedCursorOffset === value.length) { renderedValue = ( {renderedValue} @@ -89,25 +84,29 @@ const TextInput: FunctionComponent = ({ } } - let nextCursorOffset = cursorOffset + let nextCursorOffset = clampedCursorOffset let nextValue = originalValue if (key.leftArrow) { - if (cursorOffset > 0) { + if (clampedCursorOffset > 0) { nextCursorOffset-- } } else if (key.rightArrow) { - if (cursorOffset < originalValue.length) { + if (clampedCursorOffset < originalValue.length) { nextCursorOffset++ } } else if (key.backspace || key.delete) { - if (cursorOffset > 0) { - nextValue = originalValue.slice(0, cursorOffset - 1) + originalValue.slice(cursorOffset, originalValue.length) + if (clampedCursorOffset > 0) { + nextValue = + originalValue.slice(0, clampedCursorOffset - 1) + + originalValue.slice(clampedCursorOffset, originalValue.length) nextCursorOffset-- } } else { nextValue = - originalValue.slice(0, cursorOffset) + input + originalValue.slice(cursorOffset, originalValue.length) + originalValue.slice(0, clampedCursorOffset) + + input + + originalValue.slice(clampedCursorOffset, originalValue.length) nextCursorOffset += input.length }