Skip to content

Commit

Permalink
[dropdown] Extract closing transition out of the :backdrop part
Browse files Browse the repository at this point in the history
For #343
  • Loading branch information
kimo-k committed Aug 7, 2024
1 parent c9fb1b5 commit f9e9951
Show file tree
Hide file tree
Showing 5 changed files with 167 additions and 123 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@

> Committed but unreleased changes are put here, at the top. Older releases are detailed chronologically below.
## 2.21.17 (2024-08-07)

#### Added
- `dropdown` - New `:show-backdrop`? prop. Defaults to `nil`.

#### Changed

- `dropdown` - The `:backdrop` part is now purely visual. Clicking outside the anchor or body still closes the dropdown. Instead of `:backdrop`, a global event handler now handles this behavior.

## 2.21.16 (2024-08-05)

#### Added
Expand Down
202 changes: 114 additions & 88 deletions src/re_com/dropdown.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -97,10 +97,12 @@
:default "re-com.dropdown/backdrop"
:type "part"
:validate-fn part?
:description (str "Displays when the dropdown is open. By default, renders a "
"transparent overlay. Clicking this overlay closes the dropdown. "
"When a function, :backdrop is passed the same keyword arguments "
"as :anchor.")}
:description (str "Renders a visual overlay, behind the `:anchor` and `:body` parts, when the dropdown is open.")}
{:name :show-backdrop?
:required? false
:type "boolean"
:validate-fn boolean?
:description "When true, the `:backdrop` part will be rendered when the dropdown is open."}
{:name :body
:required? true
:type "part"
Expand Down Expand Up @@ -313,9 +315,12 @@
[:span {:style style}
[u/triangle {:direction (case (:openable state) :open :up :closed :down)}]])

(defn click-outside? [element event]
(let [target (.-target event)]
(not (.contains element target))))

(defn dropdown
"A clickable anchor above an openable, floating body.
"
"A clickable anchor above an openable, floating body."
[& {:keys [model] :or {model (reagent/atom nil)}}]
(let [default-model model
[focused? anchor-ref popover-ref anchor-position] (repeatedly #(reagent/atom nil))
Expand All @@ -327,6 +332,7 @@
anchor-height anchor-width
body-height body-width
model
show-backdrop?
label placeholder
anchor backdrop body body-header body-footer indicator
parts theme main-theme theme-vars base-theme
Expand All @@ -336,88 +342,108 @@
direction :toward-center}
:as args}]
(or (validate-args-macro dropdown-args-desc args)
(let [state {:openable (if (deref-or-value model) :open :closed)
:enable (if disabled? :disabled :enabled)
:tab-index tab-index
:focusable (if (deref-or-value focused?) :focused :blurred)
:transitionable @transitionable}
open! (if on-change
(handler-fn (on-change true))
#(reset! model true))
close! (if on-change
(handler-fn (on-change false))
#(reset! model false))
transition! (fn [k]
(case k
:toggle (if (-> state :openable (= :open))
(close!)
(open!))
:open (open!)
:close (close!)
:focus (reset! focused? true)
:blur (reset! focused? false)
:enter (js/setTimeout (fn [] (reset! transitionable :in)) 50)
:exit (js/setTimeout (fn [] (reset! transitionable :out)) 50)))
theme (theme/defaults
args
{:user [(theme/<-props (-> args
(dissoc :height)
(merge
(when anchor-height {:height anchor-height})
(when width {:width width})
(when anchor-width {:width anchor-width})))
{:part ::anchor-wrapper
:exclude [:max-height :min-height]})
(theme/<-props (merge args
(when height {:height height})
(when body-height {:height body-height})
(when body-width {:width body-width}))
{:part ::body-wrapper
:include [:width :height :min-width
:min-height :max-height]})
(theme/<-props args
{:part ::wrapper
:include [:class :style :attr]})]})
themed (fn [part props & [special-theme]]
(theme/apply props
{:state state
:part part
:transition! transition!}
(or special-theme theme)))
part-props {:placeholder placeholder
:transition! transition!
:label label
:theme theme
:parts parts
:state state
:indicator indicator}]
[v-box
(themed ::wrapper
{:src (at)
:children
[(when (= :open (:openable state))
[u/part backdrop
(themed ::backdrop part-props)
:default re-com.dropdown/backdrop])
[h-box
(themed ::anchor-wrapper
{:src (at)
:attr {:ref anchor-ref!}
:children [[u/part anchor (themed ::anchor part-props) :default re-com.dropdown/anchor]
[gap :size "1"]
[gap :size "5px"]
[u/part indicator part-props :default re-com.dropdown/indicator]]})]
(when (= :open (:openable state))
[body-wrapper {:anchor-ref anchor-ref
:popover-ref popover-ref
:anchor-position anchor-position
:direction direction
:parts parts
:state state
:theme theme}
[u/part body-header (themed ::body-header part-props)]
[u/part body (themed ::body part-props)]
[u/part body-footer (themed ::body-footer part-props)]])]})])))))
(let [state {:openable (if (deref-or-value model) :open :closed)
:enable (if disabled? :disabled :enabled)
:tab-index tab-index
:focusable (if (deref-or-value focused?) :focused :blurred)
:transitionable @transitionable}]
(letfn [(open! []
(on-change true)
(.addEventListener js/document "click" on-document-click)
(transition! :enter))
(open-default! []
(reset! model true)
(.addEventListener js/document "click" on-document-click)
(transition! :enter))
(close! []
(on-change false)
(.removeEventListener js/document "click" on-document-click)
(transition! :exit))
(close-default! []
(reset! model false)
(.removeEventListener js/document "click" on-document-click)
(transition! :exit))
(transition! [k]
(case k
:toggle (if (-> state :openable (= :open))
((if on-change close! close-default!))
((if on-change open! open-default!)))
:open ((if on-change open! open-default!))
:close ((if on-change close! close-default!))
:focus (reset! focused? true)
:blur (reset! focused? false)
:enter (do
(reset! transitionable :entering)
(js/setTimeout (fn [] (reset! transitionable :in)) 100))
:exit (do
(reset! transitionable :exiting)
(js/setTimeout (fn [] (reset! transitionable :out)) 100))))
(on-document-click [event]
(when (and @anchor-ref
@popover-ref
(click-outside? @anchor-ref event)
(click-outside? @popover-ref event))
(transition! :close)))]
(let [theme (theme/defaults
args
{:user [(theme/<-props (-> args
(dissoc :height)
(merge
(when anchor-height {:height anchor-height})
(when width {:width width})
(when anchor-width {:width anchor-width})))
{:part ::anchor-wrapper
:exclude [:max-height :min-height]})
(theme/<-props (merge args
(when height {:height height})
(when body-height {:height body-height})
(when body-width {:width body-width}))
{:part ::body-wrapper
:include [:width :height :min-width
:min-height :max-height]})
(theme/<-props args
{:part ::wrapper
:include [:class :style :attr]})]})
themed (fn [part props & [special-theme]]
(theme/apply props
{:state state
:part part
:transition! transition!}
(or special-theme theme)))
part-props {:placeholder placeholder
:transition! transition!
:label label
:theme theme
:parts parts
:state state
:indicator indicator}]
[v-box
(themed ::wrapper
{:src (at)
:children
[(when (and show-backdrop? (not= :out (:transitionable state)))
[u/part backdrop
(themed ::backdrop part-props)
:default re-com.dropdown/backdrop])
[h-box
(themed ::anchor-wrapper
{:src (at)
:attr {:ref anchor-ref!}
:children [[u/part anchor (themed ::anchor part-props) :default re-com.dropdown/anchor]
[gap :size "1"]
[gap :size "5px"]
[u/part indicator part-props :default re-com.dropdown/indicator]]})]
(when (= :open (:openable state))
[body-wrapper {:anchor-ref anchor-ref
:popover-ref popover-ref
:anchor-position anchor-position
:direction direction
:parts parts
:state state
:theme theme}
[u/part body-header (themed ::body-header part-props)]
[u/part body (themed ::body part-props)]
[u/part body-footer (themed ::body-footer part-props)]])]})])))))))

(defn- move-to-new-choice
"In a vector of maps (where each map has an :id), return the id of the choice offset posititions away
Expand Down
29 changes: 13 additions & 16 deletions src/re_com/theme/default.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -74,18 +74,18 @@
(case part

::dropdown/wrapper
{:attr {:on-focus #(do (transition! :focus)
(transition! :enter))
:on-blur #(do (transition! :blur)
(transition! :exit))}
{:attr {#_#_#_#_:on-focus #(do (transition! :focus)
(transition! :enter))
:on-blur #(do (transition! :blur)
(transition! :exit))}
:style {:display "inline-block"
:position "relative"}}

::dropdown/anchor-wrapper
{:attr {:tab-index (or (:tab-index state) 0)
:on-click #(transition! :toggle)
:on-blur #(do (transition! :blur)
(transition! :exit))}
#_#_:on-blur #(do (transition! :blur)
(transition! :exit))}
:style {:outline (when (and (= :focused (:focusable state))
(not= :open (:openable state)))
(str sm-2 " auto #ddd"))
Expand All @@ -100,16 +100,13 @@

::dropdown/backdrop
{:class "noselect"
:attr {:on-click #(do (transition! :close)
(transition! :blur))}
:style {:position "fixed"
:left "0px"
:top "0px"
:width "100%"
:height "100%"
#_#_:pointer-events "none"
:z-index (case (:openable state)
:open 10 nil)}}
:style {:position "fixed"
:background-color "black"
:left "0px"
:top "0px"
:width "100%"
:height "100%"
:pointer-events "none"}}

::dropdown/body-wrapper
{:ref (:ref state)
Expand Down
40 changes: 21 additions & 19 deletions src/re_demo/dropdown.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,27 @@
(:require-macros
[re-com.core :refer []])
(:require
[re-com.core :refer [at h-box v-box single-dropdown label hyperlink-href p p-span]]
[re-com.core :as rc :refer [at h-box v-box single-dropdown label hyperlink-href p p-span]]
[re-com.dropdown :refer [dropdown-parts-desc dropdown-args-desc dropdown]]
[re-demo.utils :refer [panel-title title2 title3 parts-table args-table status-text prop-slider]]
[re-demo.utils :refer [panel-title title2 title3 parts-table args-table status-text prop-slider prop-checkbox]]
[re-com.util :refer [px]]
[reagent.core :as r]))

(def model (r/atom false))

(defn panel*
[]
(let [width (r/atom 200)
height (r/atom 200)
min-width (r/atom 200)
max-width (r/atom 200)
max-height (r/atom 200)
min-height (r/atom 200)
anchor-height (r/atom 200)
body-width (r/atom 200)
(let [width (r/atom 200)
height (r/atom 200)
min-width (r/atom 200)
max-width (r/atom 200)
max-height (r/atom 200)
min-height (r/atom 200)
anchor-height (r/atom 200)
body-width (r/atom 200)
body-height (r/atom 200)
anchor-width (r/atom 200)]
anchor-width (r/atom 200)
show-backdrop? (r/atom nil)]
(fn []
[v-box :src (at) :size "auto" :gap "10px"
:children
Expand All @@ -46,13 +47,13 @@
[[title2 "Demo"]
[dropdown
(merge
{#_:anchor #_(fn [{:keys [state label] :as props}]
(str "the " label " is " (:openable state) " ;)"))
#_#_:parts {:backdrop {:style {:background-color "blue"}}}
:label "dropdown"
:body [:div "Hello World!"]
:model model
:width (some-> @width px)}
{:anchor (fn [{:keys [state label]}]
(str "This " label " is " (:openable state) (when (= :open (:openable state)) " ;)")))
:label "dropdown"
:body [:div "Hello World!"]
:model model
:width (some-> @width px)
:show-backdrop? @show-backdrop?}
(when @height {:height (px @height)})
(when @anchor-height {:anchor-height (px @anchor-height)})
(when @body-height {:body-height (px @body-height)})
Expand All @@ -73,7 +74,8 @@
[v-box :src (at)
:gap "20px"
:children
[[prop-slider {:prop width :id :width :default 212 :default-on? false}]
[[prop-checkbox {:prop show-backdrop? :id :show-backdrop?}]
[prop-slider {:prop width :id :width :default 212 :default-on? false}]
[prop-slider {:prop height :id :height :default 212 :default-on? false}]
[prop-slider {:prop min-width :id :min-width :default 212 :default-on? false}]
[prop-slider {:prop max-width :id :max-width :default 212 :default-on? false}]
Expand Down
10 changes: 10 additions & 0 deletions src/re_demo/utils.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -245,3 +245,13 @@
:width "300px"]
[gap :src (at) :size "5px"]
[label :src (at) :label (str @prop "px")]])]])))

(defn prop-checkbox [{:keys [prop default id]}]
[rc/checkbox :src (at)
:label [rc/box :src (at)
:align :start
:child [:code id]]
:model @prop
:on-change (if (some? prop)
#(swap! prop not)
#(reset! prop default))])

0 comments on commit f9e9951

Please sign in to comment.