I've created a footnote system using reagent (for any javascript people reading, that's a clojurescript wrapper over react) built on top of semantic ui (css only), which handles clicks and keyboard events as well as mouse-overs. And everything works perfectly except for one little thing: it only handles keyboard events properly once, and fails thereafter.
Code is at the bottom, but it's a little convoluted, so I'll explain it before showing it.
All the atoms are reagent atoms. The entry point is footnote, which is a component that takes the text of the footnote as well as a page marker (which is not relevant).
footnote and its downstream functions:
take a counter value from global state, increment it, and use that as the number for the footnote,
create some local state (reagent atom) to control whether a modal containing the text is visible or not, and initializes it to false,
attach a superscript footnote identifier with a tooltip to the text (for mouse-overs)
attach an on-click event handler to that footnote identifier, which sets the modal visibility atom to true, and
attach a keyboard event listener to the global window, which sets the modal visibility atom to true when the number is pressed.
Then, when a modal visibility atom is true:
another keyboard event listener is attached to the global window, that sets the modal visibility atom to false.
an on-click listener is attached to the modal itself (in a few places because I suck at DOM and don't trust my ability to figure out where one might otherwise click), also to set the modal visibility atom to false.
What I think should happen:
On render, mouse-over should show a tooltip with the text, and either clicking on the footnote number or pressing the footnote number on the keyboard should open a modal with the text. When the modal is open, clicking anywhere should close it, as should pressing any key. After closing the modal, the user should be able to open it again with either a click or pressing the number on the keyboard.
What actually happens:
Everything except the stuff in italics in the previous paragraph. Instead, after closing the modal, if I press the appropriate key to open it again, the modal does not re-open (clicking still works).
When I wrap the key handling function debugging log call, it logs "true" to the console, indicating that the atom is in the expected state.
Interestingly, re-rendering the entire virtual "page" (i.e., the higher-level component that contains the footnote component -- this is a single page site with virtual pages as react/reagent components), either by hitting refresh or just by rendering another "page" component containing different/no footnotes then re-rendering the original "page" component, seems to reset whatever kind of wonky state I'm getting, and keyboard triggers work again.
Relatedly, I have other keyboard events hanging around on the global window, but nothing that uses the same keystrokes as the footnotes. In particular, I have keyboard events that I use to navigate among "pages" (h for home, etc.) Interestingly, if I close the modal using one of those other keys, it works just fine---quite possibly because that other key re-renders one of the other pages first, then renders the original page per my forced re-rendering?
The code below (modified from when I originally posted this question) tries to take advantage of this last quirk by re-rendering the original page (that's navload), but no dice (probably because react is smart enough not to re-render the page if it's already there?).
I've also tried to force re-rendering in the close function by first rendering a dummy page then instantly rendering the original page again, but no dice---that actually breaks things worse (re-rendering no longer resets the keyboard footnote capacity.)
Finally, I've tried including reagent/force-update-all in close-modal as well as the "forcer" trick from this issue. Neither led to any change in behavior.
Other possibly pertinent information:
The listen function comes from the google closure library (goog.events), not plain javascript. Dunno if that has weird semantics or something that might be causing this problem.
Does anyone have any brilliant insights? Thanks! :-)
(defn footnote-flag [num ratom text]
[:sup {:data-tooltip text
:on-click #(reset! ratom true)}
(str "(" num ") ")])
(defn close-modal [ratom page]
(cond
(= @ratom true)
(do
(reset! ratom false)
(navload page))))
(defn modal-content [text ratom page]
(when @ratom
(do
(listen js/window "keypress" #(close-modal ratom page))
[:span.ui.dimmer.modals.page.transition.visible.active
{:on-click #(close-modal ratom page)}
[:span.ui.standard.basic.modal.transition.visible.active.scrolling
{:on-click #(close-modal ratom page)}
[:p text]]])))
(defn handle-footnote-key [key-event num page ratom]
(let [keypress (.-keyCode key-event)]
(cond
(and (= (+ 48 num) keypress) (= page @stdio.nav.curpage))
(reset! ratom true))))
(def footnote-counter (atom 0))
(defn footnote [text page]
(do
(swap! footnote-counter inc)
(let [modal-state (atom false)
num @footnote-counter]
(listen js/window "keypress" #(handle-footnote-key % num page modal-state))
(fn [text]
[:span
[footnote-flag num modal-state text]
[modal-content text modal-state page]]))))
listens inmodal-contentandfootnoteinterfering. When it's first rendered,footnoteregisters a listener to open the modal on a certain keypress. Then when the modal is registered, it's setup to close the modal on any keypress. So after the modal has rendered the first time you both open and close the modal. I'd have a look at create-class to make sure you are both creating and removing your listeners. - Walton Hoops