Overthunk

Scrolling through Athena - A fix emerges

Jul 17, 2020


Contents

Athena

This is where we left off yesterday

(= key KeyCodes.UP)
(do
    (swap! state update :index dec)
    (let [cur-sel (first (array-seq (. js/document getElementsByClassName "selected")))]
        (.. cur-sel (scrollIntoView false {:behavior "smooth" :block "center"}))))

(= key KeyCodes.DOWN)
(do
    (swap! state update :index inc)
    (let [cur-sel (first (array-seq (. js/document getElementsByClassName "selected")))]
        (.. cur-sel (scrollIntoView true {:behavior "smooth" :block "center"}))))

Now this “solution” wasn’t really all that great since there was some clear animation jankiness.

The reason it wasn’t working was because the cur-sel element was the element that’s being navigated away from. We want to scroll into the element that’s being navigated into.

(= key KeyCodes.UP)
      (do
        (swap! state update :index dec)
        (let [select-el (first (array-seq (. js/document getElementsByClassName "selected")))
              next-el (.. select-el -previousElementSibling)]
          (.. next-el (scrollIntoView true {:behavior "smooth"}))))

      (= key KeyCodes.DOWN)
      (do
        (swap! state update :index inc)
        (let [select-el (first (array-seq (. js/document getElementsByClassName "selected")))
              next-el (.. select-el -nextElementSibling)]
          (.. next-el (scrollIntoView false {:behavior "smooth"}))))

This is better! However, using scrollIntoView on the UP event snaps any element to the top of the Result list which is a little awkward. We can mitigate this by checking if the top of the bounding rectangle of the element is greater than the top of the bounding rectangle of the result list element. If thats the case, we scroll but if it isn’t we just move up the list.

WARNING - Dirty Code incoming

(= key KeyCodes.UP)
      (do
        (swap! state update :index dec)
        (let [select-el (first (array-seq (. js/document getElementsByClassName "selected")))
              next-el (.. select-el -previousElementSibling)
              athena-el (first (array-seq (. js/document getElementsByClassName "athena")))
              result-el (nth (array-seq (.. athena-el -children)) 2)
              result-box (.. result-el getBoundingClientRect)
              next-box (.. next-el getBoundingClientRect)]
          (if (< (.. next-box -top) (.. result-box -top))
            (.. next-el (scrollIntoView true {:behavior "auto"})))
          ))

      (= key KeyCodes.DOWN)
      (do
        (swap! state update :index inc)
        (let [select-el (first (array-seq (. js/document getElementsByClassName "selected")))
              next-el (.. select-el -nextElementSibling)
              athena-el (first (array-seq (. js/document getElementsByClassName "athena")))
              result-el (nth (array-seq (.. athena-el -children)) 2)
              result-box (.. result-el getBoundingClientRect)
              next-box (.. next-el getBoundingClientRect)]
          (if (> (.. next-box -bottom) (.. result-box -bottom))
            (.. next-el (scrollIntoView false {:behavior "auto"})))
          )
        )

WHEW. There’s a lot of repetitive code in there but let’s ignore that for now. The important part is the if statement -

(if (> (.. next-box -top) (.. result-box -top))
    (.. next-el (scrollIntoView false {:behavior "auto"})))

Now there’s still the case of what to do when you reach the top or bottom of the list. We leaned towards doing nothing.

The final code -

(= key KeyCodes.UP)
(when (> index 0)
    (swap! state update :index dec)
    (let [select-el (first (array-seq (. js/document getElementsByClassName "selected")))
            next-el (.. select-el -previousElementSibling)
            athena-el (first (array-seq (. js/document getElementsByClassName "athena")))
            result-el (nth (array-seq (.. athena-el -children)) 2)
            result-box (.. result-el getBoundingClientRect)
            next-box (.. next-el getBoundingClientRect)]
        (when (< (.. next-box -top) (.. result-box -top))
        (.. next-el (scrollIntoView true {:behavior "auto"})))))

(= key KeyCodes.DOWN)
(when (< index (dec (count results)))
    (swap! state update :index inc)
    (let [select-el (first (array-seq (. js/document getElementsByClassName "selected")))
            next-el (.. select-el -nextElementSibling)
            athena-el (first (array-seq (. js/document getElementsByClassName "athena")))
            result-el (nth (array-seq (.. athena-el -children)) 2)
            result-box (.. result-el getBoundingClientRect)
            next-box (.. next-el getBoundingClientRect)]
        (when (> (.. next-box -bottom) (.. result-box -bottom))
        (.. next-el (scrollIntoView false {:behavior "auto"})))))

Huzzah! Now we can scroll through the list of results with our arrow keys!

Future Optimizations

There is still a noticeable flicker when moving through the list. My hypothesis is that this is due to the color updating after the scrolling happens.

This could potentially be fixed by moving the scroll code inside the :should-component-update lifecycle method.

Takeaway

Talking through code with other people helps! And approaching a problem with a fresh outlook helps too! I spent a lot of time on this issue yesterday and got pretty much nowhere but I was able to make pretty decent progress on it today by just starting over and trying again.