React Input com palavras-chave destacáveis

Mar 10 2023
Como uma tarefa supostamente simples se transformou em uma dor de cabeça
A primeira coisa que me veio à cabeça foi que provavelmente alguém já construiu tal componente e a única coisa que tenho que fazer é adaptar a solução implementada. Após uma breve pesquisa, encontrei um exemplo feito usando textarea que funcionava exatamente como textarea com capacidade de destacar palavras separadas.
Demonstração do resultado final

A primeira coisa que me veio à cabeça foi que provavelmente alguém já construiu tal componente e a única coisa que tenho que fazer é adaptar a solução implementada. Após uma breve pesquisa, encontrei um exemplo feito usando textarea que funcionava exatamente como textarea com capacidade de destacar palavras separadas.
Além disso, as respostas no StackOverflow deixaram claro que essa não é uma tarefa simples.

Uma solução óbvia foi criar esse componente com duas partes: tag de entrada de texto na frente com texto transparente e cursor colorido e div absolutamente posicionado atrás, contendo texto estilizado visível. Ao fazer isso, já tenho um texto totalmente personalizável.

Deixarei os estilos CSS fora deste artigo porque ocupará muito espaço. O princípio básico é muito simples - propriedades de fonte, preenchimentos e tamanhos devem ser idênticos para entrada e div que está na parte de trás.

const [value, setValue] = useState('');

const onChange = (e) => setValue(e.target.value);
const format = (text) => // formatted jsx
<div className="container">
  <div className="text">{format(value)}</div>
  <input type="text" className="field" value={value} onChange={onChange} />
</div>

Ao digitar uma string muito longa, o campo começa a rolar , mas o texto em div permanece estático.

Após longas discussões com meu colega, percebemos que podemos pegar a propriedade scroll left da entrada e configurá-la para o elemento div .

// ...
const textBlockRef = useRef(null);
const [scrollValue, setScrollValue] = useState(0);
// ...
const handleScroll = (e) => setScrollValue(e.currentTarget.scrollLeft);

useEffect(() => {
  if (textBlockRef.current) {
    textBlockRef.current.scrollLeft = scrollValue;
  }
}, [scrollValue]);
<!-- ... -->
<div className="text" ref={textBlockRef}>{format(value)}</div>
<input
  type="text"
  className="field"
  value={value}
  onChange={onChange}
  onScroll={handleScroll}
 />
<!-- ... -->

o segundo problema

Acontece que não resolvi totalmente o primeiro problema e esse foi o segundo problema.

O Safari não oferece suporte à propriedade onScroll de um elemento de entrada.

Bem, isso muda o jogo (pensei), e realmente comecei a questionar minha abordagem. Mas graças a Deus descobri que o Safari suporta a propriedade onScroll de um elemento textarea .
Isso resolveu o problema.
Tudo o que eu tinha que fazer era estilizar textarea para que parecesse um input[type=”text”] e pronto.

<!-- ... -->
<textarea
  className="field"
  ref={ref}
  value={value}
  onChange={onChange}
  onScroll={handleScroll}
  rows={1}
/>
<!-- ... -->

Ao pressionar a tecla Enter, textarea cria uma nova linha. É fácil de manusear tornando o manipulador onKeyDown e cancelando o comportamento padrão Enter . Além disso, se o campo estiver envolto por uma tag de formulário também é necessário enviar a solicitação.

// ...
  const submitHandler = (e) => {
    if (e.keyCode === 13 || e.key === "Enter" || e.which === 13) {
      if (!e.repeat && e.target.form) {
        e.target.form.submit();
      }
      e.preventDefault();
    }
  };
// ...
<!-- ... -->
<textarea
  className="field"
  ref={ref}
  value={value}
  onChange={onChange}
  onScroll={handleScroll}
  onKeyDown={submitHandler}
  rows={1}
/>
<!-- ... -->

Provavelmente, mais problemas serão encontrados nesta solução e ficarei feliz se você compartilhar nos comentários o que encontrou.