Skip to content

Commit 3e08afc

Browse files
committed
[dropdown] Add polish & documentation
1 parent 111871c commit 3e08afc

File tree

7 files changed

+336
-97
lines changed

7 files changed

+336
-97
lines changed

.cljfmt.edn

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
1-
{:extra-indents {re-com.theme/apply [[:inner 0]]
2-
themed [[:inner 0]]}}
1+
{:extra-indents {re-com.theme/apply [[:inner 0]]
2+
themed [[:inner 0]]
3+
re-com.theme/<-props [[:inner 0]]}}

src/re_com/dropdown.cljs

+226-76
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
[re-com.util :as u :refer [deref-or-value position-for-id item-for-id ->v]]
99
[re-com.box :refer [align-style flex-child-style v-box h-box box]]
1010
[re-com.validate :refer [vector-of-maps? css-style? html-attr? parts? number-or-string? log-warning
11-
string-or-hiccup? position? position-options-list] :refer-macros [validate-args-macro]]
11+
string-or-hiccup? position? position-options-list part?] :refer-macros [validate-args-macro]]
1212
[re-com.popover :refer [popover-tooltip]]
1313
[clojure.string :as string]
1414
[react :as react]
@@ -19,11 +19,153 @@
1919
;; Inspiration: http://alxlit.name/bootstrap-chosen
2020
;; Alternative: http://silviomoreto.github.io/bootstrap-select
2121

22-
(defn anchor-part [{:keys [label placeholder state theme]}]
23-
[:a (theme/props {:state state :part ::anchor} theme)
24-
(or label placeholder "Select an item")])
22+
(def dropdown-parts-desc
23+
(when include-args-desc?
24+
[{:impl "[v-box]"
25+
:level 0
26+
:name :wrapper
27+
:notes "Outer wrapper."}
28+
{:name :backdrop
29+
:impl "user-defined"
30+
:level 1
31+
:notes "Transparent, clickable backdrop. Shown when the dropdown is open."}
32+
{:name :anchor-wrapper
33+
:impl "[box]"
34+
:level 1
35+
:notes "Wraps the :anchor part. Opens or closes the dropdown when clicked."}
36+
{:name :anchor
37+
:impl "user-defined"
38+
:level 2
39+
:notes "Displays the :label or :placeholder."}
40+
{:name :body-wrapper
41+
:impl "[box]"
42+
:level 1
43+
:notes "Wraps the :body part. Provides intelligent positioning."}
44+
{:name :body
45+
:impl "user-defined"
46+
:level 2
47+
:notes "Shown when the dropdown is open."}]))
48+
49+
(def dropdown-parts
50+
(when include-args-desc?
51+
(-> (map :name dropdown-parts-desc) set)))
52+
53+
(def dropdown-args-desc
54+
(when include-args-desc?
55+
[{:description "True when the dropdown is open."
56+
:name :model
57+
:required false
58+
:type "boolean | r/atom"}
59+
{:description
60+
"Called when the dropdown opens or closes."
61+
:name :on-change
62+
:required false
63+
:type "boolean -> nil"
64+
:validate-fn fn?}
65+
{:name :anchor
66+
:type "part"
67+
:validate-fn part?
68+
:required? false
69+
:description
70+
[:span "String, hiccup or function. When a function, acceps keyword args "
71+
[:code ":placholder"] ", "
72+
[:code ":label"] ", "
73+
[:code ":theme"] ", "
74+
[:code ":parts"] ", "
75+
[:code ":state"] " "
76+
" and "
77+
[:code ":transition!"]
78+
". Returns either a string or hiccup, which shows within the clickable dropdown box."]}
79+
{:name :backdrop
80+
:required? false
81+
:type "part"
82+
:validate-fn part?
83+
:description (str "Displays when the dropdown is open. By default, renders a "
84+
"transparent overlay. Clicking this overlay closes the dropdown. "
85+
"When a function, :backdrop is passed the same keyword arguments "
86+
"as :anchor.")}
87+
{:name :body
88+
:required? false
89+
:type "part"
90+
:validate-fn part?
91+
:description (str "Displays when the dropdown is open. "
92+
"Appears either above or below the :anchor, "
93+
"depending on available screen-space. When a function, "
94+
":body is passed the same keyword arguments as :anchor.")}
95+
{:name :disabled?
96+
:required false
97+
:type "boolean | r/atom"}
98+
{:default 0
99+
:description "component's tabindex. A value of -1 removes from order"
100+
:name :tab-index
101+
:required false
102+
:type "integer | string"
103+
:validate-fn number-or-string?}
104+
{:description "height of the :anchor-wrapper part"
105+
:name :anchor-height
106+
:required false
107+
:type "integer | string"
108+
:validate-fn number-or-string?}
109+
{:description "height of the :body-wrapper part"
110+
:name :height
111+
:required false
112+
:type "integer | string"
113+
:validate-fn number-or-string?}
114+
{:description "min-height of the :body-wrapper part"
115+
:name :min-height
116+
:required false
117+
:type "integer | string"
118+
:validate-fn number-or-string?}
119+
{:description "max-height of the :body-wrapper part"
120+
:name :max-height
121+
:required false
122+
:type "integer | string"
123+
:validate-fn number-or-string?}
124+
{:description "width of the :anchor-wrapper and :body-wrapper parts"
125+
:name :width
126+
:required false
127+
:type "integer | string"
128+
:validate-fn number-or-string?}
129+
{:description "min-width of the :anchor-wrapper and :body-wrapper parts"
130+
:name :min-width
131+
:required false
132+
:type "integer | string"
133+
:validate-fn number-or-string?}
134+
{:description "max-width of the :anchor-wrapper and :body-wrapper parts"
135+
:name :max-width
136+
:required false
137+
:type "integer | string"
138+
:validate-fn number-or-string?}
139+
{:description (str "passed as a prop to the :anchor part. The default :anchor "
140+
"part will display :label inside a the clickable dropdown box.")
141+
:name :label
142+
:required false
143+
:type "string | hiccup"}
144+
{:default "\"Select an item\""
145+
:description (str "passed as a prop to the :anchor part. The default :anchor part will "
146+
"show :placeholder in the clickable box if there is no :label.")
147+
:name :placeholder
148+
:required false
149+
:type "string | hiccup"}
150+
{:description "See Parts section below."
151+
:name :parts
152+
:required false
153+
:type "map"
154+
:validate-fn (parts? dropdown-parts)}
155+
{:name :theme
156+
:description "alpha"}
157+
{:name :main-theme
158+
:description "alpha"}
159+
{:name :theme-vars
160+
:description "alpha"}
161+
{:name :base-theme
162+
:description "alpha"}]))
163+
164+
(defn anchor [{:keys [label placeholder state theme transition!]}]
165+
[:a (theme/props {:state state :part ::anchor :transition! transition!} theme)
166+
(or label placeholder)])
25167

26-
(defn backdrop-part [{:keys [state transition!]}]
168+
(defn backdrop [{:keys [state transition!]}]
27169
(fn [{:keys [dropdown-open? state theme parts]}]
28170
[:div (theme/props {:transition! transition! :state state :part ::backdrop} theme)]))
29171

@@ -58,7 +200,7 @@
58200
best-y (case v-pos :low a-h :high (- p-h))]
59201
[best-x best-y]))
60202

61-
(defn body-wrapper [{:keys [state parts theme anchor-ref popover-ref anchor-position]} & children]
203+
(defn body-wrapper [{:keys [state theme anchor-ref popover-ref anchor-position]} & children]
62204
(let [set-popover-ref! #(reset! popover-ref %)
63205
optimize-position! #(reset! anchor-position (optimize-position! @anchor-ref @popover-ref))
64206
mounted! #(do
@@ -76,15 +218,12 @@
76218
(let [[left top] (or @anchor-position [0 0])]
77219
(into
78220
[:div#popover
79-
(->
80-
{:ref set-popover-ref!
81-
:style {:z-index 99999
82-
:position "absolute"
83-
:top (str top "px")
84-
:left (str left "px")
85-
:opacity (if @anchor-position 1 0)
86-
:transition "opacity 0.2s"}}
87-
(theme/apply {:state state :part ::body-wrapper} theme))]
221+
(theme/apply {}
222+
{:state (merge state {:top top
223+
:left left
224+
:ref set-popover-ref!})
225+
:part ::body-wrapper}
226+
theme)]
88227
children)))})))
89228

90229
(defn dropdown
@@ -94,72 +233,83 @@
94233
(let [[focused? anchor-ref popover-ref anchor-position] (repeatedly #(reagent/atom nil))
95234
anchor-ref! #(reset! anchor-ref %)
96235
transitionable (reagent/atom
97-
(if @model :in :out))]
236+
(if (deref-or-value model) :in :out))]
98237
(fn dropdown-render
99238
[& {:keys [disabled? on-change tab-index
100-
width height min-width max-width min-height max-height anchor-height
239+
anchor-height
240+
model
101241
label placeholder
102242
anchor backdrop body
103-
parts style theme main-theme theme-vars base-theme]
243+
parts theme main-theme theme-vars base-theme
244+
width]
245+
:or {placeholder "Select an item"}
104246
:as args}]
105-
(let [theme {:variables theme-vars
106-
:base base-theme
107-
:main main-theme
108-
:user [theme (theme/parts parts)]}
109-
state {:openable (if @model :open :closed)
110-
:enable (if disabled? :disabled :enabled)
111-
:tab-index tab-index
112-
:focusable (if @focused? :focused :blurred)
113-
:transitionable @transitionable}
114-
open! (if on-change
115-
(handler-fn (on-change true))
116-
(handler-fn (reset! model true)))
117-
close! (if on-change
118-
(handler-fn (on-change false))
119-
(handler-fn (reset! model false)))
120-
transition! (fn [k]
121-
((case k
122-
:toggle (if (-> state :openable (= :open)) open! close!)
123-
:open open!
124-
:close close!
125-
:focus #(reset! focused? true)
126-
:blur #(reset! focused? false)
127-
:enter #(js/setTimeout (fn [] (reset! transitionable :in)) 50)
128-
:exit #(js/setTimeout (fn [] (reset! transitionable :out)) 50))))
129-
themed (fn [part props] (theme/apply props
130-
{:state state
131-
:part part
132-
:transition! transition!}
133-
theme))
134-
part-props {:placeholder placeholder
135-
:transition! transition!
136-
:label label
137-
:theme theme
138-
:parts parts
139-
:state state}]
140-
[v-box
141-
(themed ::wrapper
142-
{:src (at)
143-
:style {:height anchor-height}
144-
:children
145-
[(when (= :open (:openable state))
146-
[u/part backdrop part-props backdrop-part])
147-
[box
148-
(themed ::anchor-wrapper
149-
{:src (at)
150-
:style {:padding "unset"
151-
:width "100%"}
152-
:attr {:ref anchor-ref!
153-
:on-click #(swap! model not)}
154-
:child [u/part anchor part-props anchor-part]})]
155-
(when (= :open (:openable state))
156-
[body-wrapper {:anchor-ref anchor-ref
157-
:popover-ref popover-ref
158-
:anchor-position anchor-position
159-
:parts parts
160-
:state state
161-
:theme theme}
162-
[u/part body part-props]])]})]))))
247+
(or (validate-args-macro dropdown-args-desc args)
248+
(let [state {:openable (if (deref-or-value model) :open :closed)
249+
:enable (if disabled? :disabled :enabled)
250+
:tab-index tab-index
251+
:focusable (if (deref-or-value focused?) :focused :blurred)
252+
:transitionable @transitionable}
253+
open! (if on-change
254+
(handler-fn (on-change true))
255+
#(reset! model true))
256+
close! (if on-change
257+
(handler-fn (on-change false))
258+
#(reset! model false))
259+
transition! (fn [k]
260+
(case k
261+
:toggle (if (-> state :openable (= :open))
262+
(close!)
263+
(open!))
264+
:open (open!)
265+
:close (close!)
266+
:focus (reset! focused? true)
267+
:blur (reset! focused? false)
268+
:enter (js/setTimeout (fn [] (reset! transitionable :in)) 50)
269+
:exit (js/setTimeout (fn [] (reset! transitionable :out)) 50)))
270+
theme {:variables theme-vars
271+
:base base-theme
272+
:main main-theme
273+
:user [theme
274+
(theme/parts parts)
275+
(theme/<-props (merge args {:height anchor-height})
276+
{:part ::anchor-wrapper
277+
:exclude [:max-height :min-height]})
278+
(theme/<-props args
279+
{:part ::body-wrapper
280+
:include [:width :min-width
281+
:min-height :max-height]})]}
282+
themed (fn [part props & [special-theme]]
283+
(theme/apply props
284+
{:state state
285+
:part part
286+
:transition! transition!}
287+
(or special-theme theme)))
288+
part-props {:placeholder placeholder
289+
:transition! transition!
290+
:label label
291+
:theme theme
292+
:parts parts
293+
:state state}]
294+
[v-box
295+
(themed ::wrapper
296+
{:src (at)
297+
:children
298+
[(when (= :open (:openable state))
299+
[u/part backdrop part-props re-com.dropdown/backdrop])
300+
[box
301+
(themed ::anchor-wrapper
302+
{:src (at)
303+
:attr {:ref anchor-ref!}
304+
:child [u/part anchor part-props re-com.dropdown/anchor]})]
305+
(when (= :open (:openable state))
306+
[body-wrapper {:anchor-ref anchor-ref
307+
:popover-ref popover-ref
308+
:anchor-position anchor-position
309+
:parts parts
310+
:state state
311+
:theme theme}
312+
[u/part body part-props]])]})])))))
163313

164314
(defn- move-to-new-choice
165315
"In a vector of maps (where each map has an :id), return the id of the choice offset posititions away

0 commit comments

Comments
 (0)