Skip to content

Instantly share code, notes, and snippets.

@Alex-Bakic
Last active March 28, 2020 17:39
Show Gist options
  • Save Alex-Bakic/543e0d6a389cab7941e373822804ced6 to your computer and use it in GitHub Desktop.
Save Alex-Bakic/543e0d6a389cab7941e373822804ced6 to your computer and use it in GitHub Desktop.

WorksHub Issue 30: Setting up the keydown event for drop-down select components

This issue covers the remastering of the drop-down component , and includes things like the introduction of keydown, click and close events.

As we need to reference the DOM element itself, we need to use some underlying javascript APIs to get this done, and so we will need to interop at some point. To add event listeners to items we need to make sure that those items are already rendered and part of the web page, so doing something like this wouldn't work:

(into [:ul {:id "dropdown--options" :tabindex "0"}]
          (map (fn [{:keys [id label]}]
                 (let [unique-id (str "dropdown--item--" id)]
                   [:li {:id unique-id
                       :tabindex "0"}
                    label]
                    ;; calling the event listeners whilst defining markup
                    ;; will mean the event listeners get called IMMEDIATELY , and fail...
                    (.addEventListener (.getElementById js/document unique-id) "click" (fn [] ...))))
               items))

Instead, we need to be sure that the component we define has rendered before we plaster on our listener code, and for this we will need the power of lifecycle methods. Though there are other ways to do this, which don't involve a form-3 component , we compromise the flexbility of our component code and we would have to redefine our views in simpler terms.

By using reagent/create-class we can know the code gets rendered within :reagent-render , and that we can safely add our listeners within :component-did-mount.

The first thing we'll do is define the schematics of our dropdown

(defn dropdown-options
  [items set-dropdown-ev]
  ;; the :id key bears another map, and the label is unique enough
  ;; adding the unique ids before looping and creating the view itself
  (let [unique-ids (reduce (fn [val item] (conj val (:label item))) [] items)]
    (r/create-class
     {:display-name "dropdown--options"
      :reagent-render (fn [items set-dropdown-ev] ... )

      ;; now for the event listeners added on mount
      :component-did-mount (fn [this] ...)

When the input loads, not the list itself, that will be focused , and should respond to changes within that input. So we'll need to add an eventListener within the input, that watches out for the keydown event. We already handle click within each li element, so it's just things like arrow-up, arrow-down and enter that we'll need to create here

;; the fn for the :component-did-mount method
(fn [this]
 (let [input (.getElementById js/document "dropdown__input")
       dropdown (.getElementById js/document "dropdown__options")
       dropdown-items (.querySelectorAll dropdown "li")
       length (.-length dropdown-items)
       counter (atom 0)]
   (do
     ;; first set event listener for the input
     (.addEventListener input "keydown" (partial keydownhandler items dropdown-items set-dropdown-ev set-input-ev))

     ;; then all the listeners for each item
     (while (< @counter length)
       (do
         (.addEventListener (.item dropdown-items @counter) "keydown" (partial keydownhandler items dropdown-items set-dropdown-ev set-input-ev))
         (swap! counter inc))))))

Now the handler itself should only worry about the up, down and enter keys:

;; handler for the input event, to focus 
(defn keydownhandler
  [items dropdown-items set-dropdown-ev set-input-ev event]
  (let [keycode (.-keyCode event)]
    ;; codes is just a map that holdes keycodes : found in common/src/wh/common/keycodes
    (cond
      (= keycode (:down codes))  (focus-item :down dropdown-items)
      (= keycode (:up codes))    (focus-item :up dropdown-items)
      (= keycode (:enter codes)) (submit-focused-item event set-dropdown-ev items))))

Now comes the trickiest part: focusing.

For a given web page, there may be many parts that require focusing, like the inputs and options we have here. Though the input should be initially focused and not the dropdown, which should only pop up when a user starts typing. Typically the browser looks after this "focusing hierarchy", but as we're creating functionaliy on top of our input, we need to manage this ourselves.

This is done through the tabIndex attribute, which can take a numerical value and describes an elements position in the "focus hierarchy". A value of -1 would mean that an element is not focusable and not part of the hierarchy, similar to doing position: absolute; for the display hierarchy. This gives us control of what item is focused on, and the active element would then need to have a tabIndex of 0 to be focused on.

;; to handle the tabIndex for every option
(defn focus
  "first argument are the actual html li objects, and the second arg is the item we want focused"
  [dropdown-items item]
  ;; first set tabIndex all to -1
  (let [counter (atom 0)]
    (while (< @counter (.-length dropdown-items))
      (do
        (set! (.-tabIndex (.item dropdown-items @counter)) "-1")
        (swap! counter inc)))
    (set! (.-tabIndex item) "0")
    (.focus item)))

Now that each individual li knows where it stands, we need to regulate how far down , or how far up , a user can scroll through items. Which translates to - "if the down arrow is pressed, and the item to be focused isn't the last item, then focus it". Likewise , "if the up arrow is pressed, and the item to be focused wouldn't be the first item, then focus it".

(defn focus-item
  "takes the direction keydownhandler and checks if we can make that shift"
  [direction dropdown-items]
  (let [input-active? (= "dropdown__input" (.-id (.-activeElement js/document)))]
    (if input-active?
      (focus dropdown-items (.item dropdown-items 0))
      (let [index (js/parseInt (.-index (.-dataset (.-activeElement js/document))))]
        (cond
          (and (= direction :down) (<= index 3)) (focus dropdown-items (.item dropdown-items (inc index)))
          (and (= direction :up) (> index 0))    (focus dropdown-items (.item dropdown-items (dec index))))))))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment