diff --git a/CHANGELOG.md b/CHANGELOG.md index e9092c7a..c5e770cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/re_com/dropdown.cljs b/src/re_com/dropdown.cljs index 3e6152a8..eecfd718 100644 --- a/src/re_com/dropdown.cljs +++ b/src/re_com/dropdown.cljs @@ -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" @@ -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)) @@ -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 @@ -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 diff --git a/src/re_com/theme/default.cljs b/src/re_com/theme/default.cljs index cfaf7ce4..cefabaf8 100644 --- a/src/re_com/theme/default.cljs +++ b/src/re_com/theme/default.cljs @@ -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")) @@ -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) diff --git a/src/re_demo/dropdown.cljs b/src/re_demo/dropdown.cljs index 727f6a22..87e9504a 100644 --- a/src/re_demo/dropdown.cljs +++ b/src/re_demo/dropdown.cljs @@ -2,9 +2,9 @@ (: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])) @@ -12,16 +12,17 @@ (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 @@ -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)}) @@ -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}] diff --git a/src/re_demo/utils.cljs b/src/re_demo/utils.cljs index 178919f6..453c447a 100644 --- a/src/re_demo/utils.cljs +++ b/src/re_demo/utils.cljs @@ -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))])