1

I've seen this question asked and answered, but the answers are never applicable to my issue, meaning they don't really solve the issue, and they're old so it's possible new solutions exist.

I have textareas that all start out with 1 row. User input causes the tetxareas to grow to fit content. I want the user to be able to move between textareas (they're stacked vertically) using the arrow keys. The issue is, I only want the UP ARROW to move to previous textarea if caret is in the first line. Same goes for DOWN ARROW/bottom line.

First thing to note: looking for \n doesn't help because it isn't a new line, just wrapped text to fit into the textarea.

Second: I don't know if it's relevant info, but the width of textareas would be fixed, so maybe it's possible to get caret position and do some math to calculate if it's in the first row, although I'm not sure if that works because of word rap (i.e., the first row could contain x characters, but is wrapped and now only has y).

You would think that it's built in. When you hit UP ARROW when you're in the first line it moves caret to start of content. Is there a way to utilize that logic? I'm assuming it's built in. Same goes for the DOWN ARROW.

Again, I have seen this question asked but couldn't use any of the answers for my specific case. The textareas I'm using are not styled and the only info that's helpful is that they start with 1 row and are resized dynamically using this function:

  const resizeTextarea = (textarea) => {
    textarea.style.boxSizing = "border-box";
    textarea.style.height = "auto";
    textarea.style.height = `${textarea.scrollHeight}px`;
  };

The textareas are also saved as objects in an array using useState. Should I maybe add some sort of increment to the object when it dynamically grows to fit content (calls this function) so that when a user hits an arrow key, I could tell how many "lines" are in that textarea.

Potential issue is that I still don't know how to figure out if the caret is in that first row/last row even if I know the amount of rows. I mean, I already know the clientHeight and that couldn't lead me to the promise land...

You would think that it's built in. When you hit UP ARROW when you're in the first line it moves caret to start of content. Is there a way to utilize that logic? I'm assuming it's built in. Same goes for the DOWN ARROW.

This part really interests me, but I'm not sure what code (or whose) is even doing it.

Yehuda
  • 27
  • 15
  • https://stackoverflow.com/questions/7745867/how-do-you-get-the-cursor-position-in-a-textarea – epascarello Mar 06 '23 at 16:52
  • The guy had a similar question but I couldn't work the answers – Yehuda Mar 07 '23 at 04:07
  • You're describing non-standard behavior which duplicates native browser functionality (tabbed navigation). This introduces a significant usability problem which should require an equally significant justification to implement. Otherwise, your invented behavior is hidden, unexpected, can become irritating, and could be perceived as buggy software. – Nate Whittaker Mar 15 '23 at 18:53
  • That's a bit above my pay grade in understanding. Just not seeing why it's such a difficult coding problem. – Yehuda Mar 15 '23 at 21:17

3 Answers3

4

This is not a solution, but a suggestion.

Instead of doing something custom just use the already existing and accessible keyboard navigation. TAB for next element and SHIFT + TAB for previous. Anyone who seriously uses a keyboard to navigate must know these.

In my opinion what you are describing would be extremely unexpected behavior for users.

If you want, you can control the element ordering when using tab-based navigation via tabindex. However, based on this

they're stacked vertically

you probably shouldn't even use that.


what code (or whose) is even doing it

The behavior you have described as well as the use of keyboard shortcuts such as tab is implemented by the browser. e.g. from the Firefox documentation,

Current Page
Command Shortcut
Focus Next Link or Input Field Tab
Focus Previous Link or Input Field Shift + Tab

https://support.mozilla.org/en-US/kb/keyboard-shortcuts-perform-firefox-tasks-quickly#w_current-page

However, credible browsers base their implementations off of well-defined and codified standards for the web. These standards are ubiquitous.

See https://www.w3.org/WAI/perspective-videos/keyboard/
and heavy reading also linked to by that page https://www.w3.org/WAI/WCAG21/quickref/?tags=keyboard

Ders
  • 1,068
  • 13
  • 16
  • What I'm working on is a text editor of sorts. So each textarea is a new line in the doc and the user should be able to move around with the keys. By moving up through the lines in a textarea and to the previous one if they're in the first row. – Yehuda Mar 14 '23 at 14:11
1
  1. Get caret position by selectionStart

  2. If the caret position = 0, move to previous textarea.

  3. If the caret position = textLength, move to next textarea.

  4. Execute the function when keyup

const textarea = document.getElementsByTagName("textarea");
Array.prototype.forEach.call(textarea, (e) => e.addEventListener("keyup", (e) => caret(e)));

function caret(e) {
  let focus = document.querySelector("textarea:focus");
  let focus_i = [...focus.parentNode.children].indexOf(focus);
  switch (e.keyCode) {
    case 38:
      if (focus.selectionStart === 0) focus_i = focus_i - 1 < 0 ? focus_i : focus_i - 1;
      break;
    case 40:
      if (focus.selectionStart === focus.textLength) focus_i = focus_i + 1 >= textarea.length ? focus_i : focus_i + 1;
      break;
  }
  textarea[focus_i].focus();
}
textarea {
  display: block;
}
<textarea></textarea>
<textarea></textarea>
<textarea></textarea>
Shuo
  • 1,512
  • 1
  • 3
  • 13
  • If you're in the middle of the textarea and you hit up it should move you to the previous textarea. This only moves if you're at the start. It also doesn't take into account what I mentioned I needed, which was a way to tell which line you're in. – Yehuda Mar 10 '23 at 14:42
  • That's why I use `keyup`. When press `up arrow` at first line, caret jumps to start, then the `up arrow` released will execute `caret()`, move to previous textarea. – Shuo Mar 11 '23 at 11:00
  • But I had no idea how to detect which line the caret in. – Shuo Mar 11 '23 at 11:03
0

This is not easy to achieve since you have to face several issues and I think the best solution should be to rethink the UI.

Anyway, if you really want to do this, I would recommend to use a library to get the position of the caret on the screen. Such libraries are used to create web text editors with syntax highlighting or other advanced features, so you have a good chance to find a solid one.

Once you have the position of the caret, you can compare it with the ones you get if you move the cursor to the beginning or to the end of the text. In that way you would know whether the caret lies on the first or on the last line.

Here is an example implementation using Textarea Caret Position:

function get_caret_top(el, pos) {
  // Return the y position that the caret in the textarea 'el'
  // would have if:
  // - both selectionStart and selectionEnd would be set to 'pos'
  // - the textarea would lie in the top left corner of the page
  // - the height of the textarea would grow to fit its content
  const clone = el.cloneNode(true)
  clone.removeAttribute("id")
  clone.selectionStart = pos
  clone.selectionEnd = pos
  clone.style.visibility = "hidden"
  clone.style.position = "absolute"
  clone.style.left = "0"
  clone.style.top = "0"
  document.body.appendChild(clone)
  clone.style.height = "0"
  clone.style.height = clone.scrollHeight + "px"
  const caret = getCaretCoordinates(clone, clone.selectionStart)
  clone.remove()
  return caret.top
}

function get_caret_line(el) {
  // Check if the caret is on the first and/or on the
  // last line of the textarea 'el'.
  // Return an object in the form:
  // { is_first: true|false, is_last: true|false }
  const first_line_h = get_caret_top(el, 0)
  const last_line_h = get_caret_top(el, el.value.length)
  const caret_h = get_caret_top(el, el.selectionStart)
  const is_first = first_line_h == caret_h
  const is_last = last_line_h == caret_h
  return { is_first, is_last }
}

document.querySelectorAll('.entry').forEach(el => {
  el.addEventListener('keydown', e => {
    const entry = e.target
    const { is_first, is_last } = get_caret_line(entry)
    if (e.keyCode == 40) { // arrow down
      if (!is_last) return
      if (!entry.dataset.next) return
      document.querySelector(entry.dataset.next).focus()
    }
    if (e.keyCode == 38) { // arrow up
      if (!is_first) return
      if (!entry.dataset.prev) return
      document.querySelector(entry.dataset.prev).focus()
    }
  })
})
.entry {
  width: 300px;
  height: 100px;
  display: block;
}
<script src="https://rawgit.com/component/textarea-caret-position/master/index.js"></script>

<textarea class="entry" id="txt1" data-next="#txt2">Dolor repellendus ratione aut voluptatem dolore. Architecto aliquid quisquam temporibus iusto explicabo non nisi? Veritatis dignissimos ut consectetur repellendus doloribus. Vero qui neque sunt magnam nobis! Veniam aliquid iure eligendi.</textarea>

<textarea class="entry" id="txt2" data-prev="#txt1">Sit velit aut debitis tenetur ipsum incidunt et Mollitia sed unde aliquam consequuntur rem Reiciendis ducimus praesentium laborum magni quibusdam Culpa magnam quia debitis nemo ipsam ducimus. Qui nisi nam.</textarea>
etuardu
  • 5,066
  • 3
  • 46
  • 58
  • I just find it so odd that the browser has code that can do this perfectly, since it can move the caret to the start if UP ARROW + in first line. I'm new to programming, but is this normal? Like, how is the browser doing it? You would think it would be built in. – Yehuda Mar 14 '23 at 21:16
  • The browser doesn't have to implement everything at the finest granularity level. It may use components provided by a lower layer so it may even ignore that bit of information. – etuardu Mar 14 '23 at 21:54
  • Then how is it capable of doing this exact thing seamlessly. Whether it's css wrapped or an actual new line break. – Yehuda Mar 14 '23 at 21:58