From 80a01c5ea27852cd78da829799d3fa976f8a6571 Mon Sep 17 00:00:00 2001 From: Rob Date: Tue, 30 Jun 2026 14:09:38 +1000 Subject: [PATCH 1/3] Fix popover :style/:parts precedence; document :title-bar rename The 2.29.4 popover parts/theme rewrite applied each component's computed styles via :post-props, which outranks a caller's :style, so user :style/:parts overrides were silently lost (e.g. :opacity, :max-width, position). Restore the documented "user styling wins" contract, matching modal-panel / input-text: - popover-border, popover-content-wrapper: build the computed styles from {} first, then apply the caller's :class/:style/:attr last. - popover-tooltip: move the v-box's computed colour/font :style into :props so a caller's :parts {:v-box ...} wins. Also document the breaking :parts key rename :title -> :title-bar on popover-content-wrapper, and correct the changelog's "preserved verbatim" wording, which the rename made inaccurate. Add re-com.popover-test covering the precedence contract. --- CHANGELOG.md | 5 ++++- src/re_com/popover.cljs | 32 ++++++++++++++++++-------------- test/re_com/popover_test.cljs | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 55 insertions(+), 15 deletions(-) create mode 100644 test/re_com/popover_test.cljs diff --git a/CHANGELOG.md b/CHANGELOG.md index 397c546f..6e01b8ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,10 @@ ## 2.29.4 (Unreleased) #### Added -- `popover`: full `:parts` & `:theme` support for all five popover components (`popover-title`, `popover-border`, `popover-content-wrapper`, `popover-anchor-wrapper`, `popover-tooltip`). Closes the gap called out in the 2.29.0 changelog ("Full `:parts` & `:theme` support for most components (excluding `popover`, `v-table` and `simple-v-table`)"). The classic look is preserved verbatim — every legacy CSS class (`rc-popover-border`, `popover`, `rc-popover-title`, `rc-popover-arrow`, `rc-popover-content`, `rc-popover-anchor-wrapper`, `rc-point-wrapper`, `rc-popover-point`, `rc-popover-tooltip`, etc.) is still applied via `bootstrap` theme methods. Form-3 lifecycle behaviour (refs, `:component-did-mount`, `:component-did-update`, position-optimization atoms) is unchanged; `theme/comp` is composed once at mount time per component instance. +- `popover`: full `:parts` & `:theme` support for all five popover components (`popover-title`, `popover-border`, `popover-content-wrapper`, `popover-anchor-wrapper`, `popover-tooltip`). Closes the gap called out in the 2.29.0 changelog ("Full `:parts` & `:theme` support for most components (excluding `popover`, `v-table` and `simple-v-table`)"). The classic visual appearance is unchanged — every legacy CSS class (`rc-popover-border`, `popover`, `rc-popover-title`, `rc-popover-arrow`, `rc-popover-content`, `rc-popover-anchor-wrapper`, `rc-point-wrapper`, `rc-popover-point`, `rc-popover-tooltip`, etc.) is still applied via `bootstrap` theme methods. Form-3 lifecycle behaviour (refs, `:component-did-mount`, `:component-did-update`, position-optimization atoms) is unchanged; `theme/comp` is composed once at mount time per component instance. + +#### Changed +- `popover-content-wrapper`: the `:parts` key for the title bar (the inner `popover-title`) is renamed `:title` → `:title-bar`. **Breaking** for any code styling the title via `:parts {:title ...}` on `popover-content-wrapper` — directly, or through `popover-anchor-wrapper`'s `:popover`: change it to `:parts {:title-bar ...}`. In dev builds the old key now raises a validation error (`Invalid keyword in :parts parameter: :title`); in production it is silently ignored. The rename disambiguates the part from the `:title` argument (the title text/hiccup), which the modern parts system would otherwise shadow. The title text, its default styling, and all CSS classes are unchanged. #### Fixed - `popover-anchor-wrapper`: `:popover` argument using positional-args calling style `[popover-fn arg1 arg2 ...]` no longer breaks `:showing-injected?`/`:position-injected` injection. Previously the non-keyword branch wrapped the call as a single map, causing the receiving fn's `[a b & {:keys [...]}]` destructure to bind `a` to the entire map and produce nil kwargs — visible as a 💥 in the "Complex Popover (dialog box)" demo. Map-style invocation `[popover-fn {props}]` continues to work. [#367](https://github.com/day8/re-com/issues/367) diff --git a/src/re_com/popover.cljs b/src/re_com/popover.cljs index da566889..ee407f39 100644 --- a/src/re_com/popover.cljs +++ b/src/re_com/popover.cljs @@ -329,9 +329,7 @@ :popover-color popover-color}}] (part ::pb/wrapper {:theme theme - :post-props (-> args - (select-keys [:class :style :attr]) - (assoc-in [:attr :id] pop-id) + :post-props (-> {} (tu/style (if @rendered-once (when pop-id (calc-popover-pos orientation @p-width @p-height @pop-offset arrow-length arrow-gap)) {:top "-10000px" :left "-10000px"})) @@ -351,6 +349,10 @@ :opacity (if @ready-to-show? "1" "0") :max-width "none" :padding "0px"}) + (cond-> (:class args) (tu/class (:class args)) + (:style args) (tu/style (:style args)) + (:attr args) (update :attr merge (:attr args))) + (assoc-in [:attr :id] pop-id) (debug/instrument args) (assoc-in [:attr :ref] ref!)) :props {:re-com re-com @@ -489,11 +491,13 @@ :showing? (deref-or-value showing-injected?)}}] (part ::pcw/wrapper {:theme theme - :post-props (-> args - (select-keys [:class :style :attr]) + :post-props (-> {} (cond-> no-clip? (tu/style {:position "fixed" :left (px @left-offset) :top (px @top-offset)})) + (cond-> (:class args) (tu/class (:class args)) + (:style args) (tu/style (:style args)) + (:attr args) (update :attr merge (:attr args))) (debug/instrument args) (assoc-in [:attr :ref] ref!)) :props {:re-com re-com @@ -735,17 +739,17 @@ (part ::ptip/v-box {:impl v-box :theme theme - :post-props {:src (at) - :style (if (= status :info) - {:color "white" - :font-size "14px" - :padding "4px"} - {:color "white" - :font-size "12px" - :font-weight "bold" - :text-align "center"})} + :post-props {:src (at)} :props {:re-com re-com + :style (if (= status :info) + {:color "white" + :font-size "14px" + :padding "4px"} + {:color "white" + :font-size "12px" + :font-weight "bold" + :text-align "center"}) :children [(when close-button? (part ::ptip/close-button-container diff --git a/test/re_com/popover_test.cljs b/test/re_com/popover_test.cljs new file mode 100644 index 00000000..6f5d1bf0 --- /dev/null +++ b/test/re_com/popover_test.cljs @@ -0,0 +1,33 @@ +(ns re-com.popover-test + (:require + [cljs.test :refer-macros [is deftest testing]] + [re-com.part :as part])) + +;; --------------------------------------------------------------------------- +;; Style-precedence contract that the popover :style fix relies on: +;; a user's :style (via :post-props or :parts) must win over a component's +;; own :style. Mirrors the pure composition tests in theme_test / part_test. +;; --------------------------------------------------------------------------- + +(def structure + [::wrapper + [::border]]) + +(deftest user-post-props-style-wins-over-component-style + (testing "a user :style (carried in :post-props) overrides a colliding component :style" + (let [[_ props] (part/part structure {} ::border + {:props {:style {:opacity "0" :max-width "none" :color "black"}} + :post-props {:style {:opacity "0.42" :max-width "137px"}}})] + (is (= "0.42" (get-in props [:style :opacity]))) + (is (= "137px" (get-in props [:style :max-width]))) + (is (= "black" (get-in props [:style :color])) "non-colliding component style is preserved")))) + +(deftest user-parts-style-wins-over-component-style + (testing "a user :parts {part {:style …}} overrides a colliding component :style in :props" + (let [[_ props] (part/part structure + {:parts {:border {:style {:color "white" :font-size "12px"}}}} + ::border + {:props {:style {:color "black" :font-size "14px" :font-weight "bold"}}})] + (is (= "white" (get-in props [:style :color]))) + (is (= "12px" (get-in props [:style :font-size]))) + (is (= "bold" (get-in props [:style :font-weight])) "non-colliding component style is preserved")))) From 9ce8d2b13a49fd94e65fa1e407223f4bac917afa Mon Sep 17 00:00:00 2001 From: Rob Date: Tue, 30 Jun 2026 15:05:16 +1000 Subject: [PATCH 2/3] Add opt-in column-width resizing to simple-v-table Pass :resizable-columns? true to add a draggable grip to each column's right edge; drag it to resize the column live. Defaults to false. - A column opts out with :resizable? false, and sets a minimum drag width (px) with :min-width (default 10), in its :columns entry. - :on-resize is invoked on each drag step with {:column-id :width} so consumers can observe or persist widths; resizing works without it. - Dragged widths are held internally (keyed by column :id) and are ephemeral (not persisted across remounts). - Adds the stylable :simple-column-header-resize-handle part. - A full-viewport overlay captures mouse-move/up during the drag so it keeps tracking when the pointer leaves the grip. Adapted from nested-grid's resize affordance. The Sales Table demo gains a :resizable-columns? toggle. --- CHANGELOG.md | 1 + demo/re_demo/simple_v_table.cljs | 2 +- demo/re_demo/simple_v_table_sales.cljs | 13 ++ src/re_com/simple_v_table.cljs | 210 +++++++++++++++++++------ 4 files changed, 175 insertions(+), 51 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e01b8ac..6cfe106c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## 2.29.4 (Unreleased) #### Added +- `simple-v-table`: opt-in column-width resizing. Pass `:resizable-columns? true` to add a draggable grip to each column's right edge; drag it to resize the column live. Backwards compatible — it defaults to `false`, so existing tables render and behave exactly as before. A column can opt out with `:resizable? false`, and set a minimum drag width (px) with `:min-width` (default 10), in its `:columns` map. An optional `:on-resize` callback receives `{:keys [column-id width]}` on each drag step so consumers can observe or persist widths. Resized widths are held internally (keyed by column `:id`) and are ephemeral (not persisted across remounts). Adds the stylable `:simple-column-header-resize-handle` part. Adapted from `nested-grid`'s resize affordance. - `popover`: full `:parts` & `:theme` support for all five popover components (`popover-title`, `popover-border`, `popover-content-wrapper`, `popover-anchor-wrapper`, `popover-tooltip`). Closes the gap called out in the 2.29.0 changelog ("Full `:parts` & `:theme` support for most components (excluding `popover`, `v-table` and `simple-v-table`)"). The classic visual appearance is unchanged — every legacy CSS class (`rc-popover-border`, `popover`, `rc-popover-title`, `rc-popover-arrow`, `rc-popover-content`, `rc-popover-anchor-wrapper`, `rc-point-wrapper`, `rc-popover-point`, `rc-popover-tooltip`, etc.) is still applied via `bootstrap` theme methods. Form-3 lifecycle behaviour (refs, `:component-did-mount`, `:component-did-update`, position-optimization atoms) is unchanged; `theme/comp` is composed once at mount time per component instance. #### Changed diff --git a/demo/re_demo/simple_v_table.cljs b/demo/re_demo/simple_v_table.cljs index 2a86adaf..1f52c12b 100644 --- a/demo/re_demo/simple_v_table.cljs +++ b/demo/re_demo/simple_v_table.cljs @@ -29,6 +29,7 @@ [:li "Unlimited (virtualised) rows with an (optional) fixed row header at the left by simply specifying the number of columns to fix"] [:li "Click on a column header to sort the rows in a column"] [:li "Shift-click on a column header to sort multiple columns hierarchically."] + [:li "Opt in to column-width resizing with " [:code ":resizable-columns? true"] ". A draggable grip appears on each column's right edge. Off by default; per-column opt-out via " [:code ":resizable? false"] "."] [:li "Most aspects of the table are stylable using the " [:code ":parts"] " argument that can set " [:code ":class"] " or " [:code ":style"] " attributes"] [:li "Individual rows can be dynamically styled based on row data"] [:li "Individual cells can be dynamically styled based on row data"] @@ -36,7 +37,6 @@ [title3 "Not Implemented"] [p "The following features common to other table widgets are not implemented. After all, this is 'simple' v-table."] [:ul - [:li "Resizable columns; e.g. drag to resize columns"] [:li "Re-orderable columns; e.g. drag to re-order columns"] [:li "Hide/show columns"] [:li "Selection of rows, columns and cells (as supported by v-table)"] diff --git a/demo/re_demo/simple_v_table_sales.cljs b/demo/re_demo/simple_v_table_sales.cljs index 79493c27..7a38860c 100644 --- a/demo/re_demo/simple_v_table_sales.cljs +++ b/demo/re_demo/simple_v_table_sales.cljs @@ -54,6 +54,7 @@ fixed-column-count? (reagent/atom true) fixed-column-count (reagent/atom 1) striped? (reagent/atom false) + resizable-columns? (reagent/atom false) parent-color "#BEEDFF" parent-width? (reagent/atom false) parent-width (reagent/atom 600) @@ -197,6 +198,17 @@ [label :src (at) :label [:code ":striped?"]]]] + [h-box + :src (at) + :gap spacing7 + :align :center + :children [[checkbox + :src (at) + :model resizable-columns? + :on-change #(reset! resizable-columns? %)] + [label + :src (at) + :label [:code ":resizable-columns?"]]]] [gap :src (at) :size "0px"] @@ -345,6 +357,7 @@ ;; ===== Styling :striped? @striped? + :resizable-columns? @resizable-columns? :cell-style (fn [{:keys [sales] :as row} {:keys [id] :as column}] (when (= :sales id) {:background-color (cond diff --git a/src/re_com/simple_v_table.cljs b/src/re_com/simple_v_table.cljs index 16647c4f..cec7b6c9 100644 --- a/src/re_com/simple_v_table.cljs +++ b/src/re_com/simple_v_table.cljs @@ -65,10 +65,57 @@ :right :end :center :center}) +(defn drag-overlay + "While a column is being resized, this full-viewport, fixed-position overlay + captures mouse-move/up so the drag keeps tracking even when the pointer leaves + the (narrow) resize handle. Adapted from re-com.nested-grid." + [{:keys [on-mouse-move on-mouse-up]}] + [:div {:on-mouse-move on-mouse-move + :on-mouse-up on-mouse-up + :style {:position "fixed" + :top 0 + :left 0 + :z-index 2147483647 + :width "100%" + :height "100%" + :cursor "col-resize"}}]) + +(defn resize-handle + "An opt-in draggable grip on a column's right edge. Adapted from + re-com.nested-grid's resize affordance. Stylable/overridable via the + :simple-column-header-resize-handle part." + [_] + (let [hover? (reagent/atom nil)] + (fn [{:keys [parts on-mouse-down]}] + [:div + (merge + {:class (theme/merge-class "rc-simple-v-table-column-header-resize-handle" + (get-in parts [:simple-column-header-resize-handle :class])) + :on-mouse-down on-mouse-down + :on-mouse-over #(reset! hover? true) + :on-mouse-out #(reset! hover? false) + :style (merge {:position "absolute" + :top 0 + :right 0 + :width "9px" + :height "100%" + :cursor "col-resize" + :user-select "none" + :z-index 1 + ;; A persistent faint divider marks each column's draggable + ;; right edge (addresses "where do I grab?"); it highlights on + ;; hover. Border width is constant (1px) so there's no layout + ;; shift, only a colour change. + :border-right (if @hover? "1px solid #2684ff" "1px solid rgba(0, 0, 0, 0.18)") + :background (if @hover? "rgba(38, 132, 255, 0.15)" "transparent")} + (get-in parts [:simple-column-header-resize-handle :style]))} + (get-in parts [:simple-column-header-resize-handle :attr]))]))) + (defn column-header-item [& _] - (fn [{:keys [parts sort-by-column hover? column-header-height] - {:keys [id row-label-fn width height align header-label sort-by]} :column}] + (fn [{:keys [parts sort-by-column hover? column-header-height + resizable-columns? column-widths overlay on-resize] + {:keys [id row-label-fn width height align header-label sort-by resizable? min-width]} :column}] (let [sort-by (cond (true? sort-by) {} :else sort-by) default-sort-by {:key-fn row-label-fn :comp compare :id id :order :asc} ps (position-for-id id @sort-by-column) @@ -79,47 +126,68 @@ (add-criteria!) (replace-criteria!)) justify (get align->justify (keyword align) :start) - multiple-columns-sorted? (> (count @sort-by-column) 1)] + multiple-columns-sorted? (> (count @sort-by-column) 1) + resizable? (and resizable-columns? (not (false? resizable?))) + start-resize (fn [e] + (.preventDefault e) + (.stopPropagation e) + (let [start-x (.-clientX e) + start-width (or (get @column-widths id) width 25) + min-w (or min-width 10)] + (reset! overlay + [drag-overlay + {:on-mouse-up (fn [_] (reset! overlay nil)) + :on-mouse-move (fn [ev] + (.preventDefault ev) + (let [dx (- (.-clientX ev) start-x) + new-width (max min-w (+ start-width dx))] + (swap! column-widths assoc id new-width) + (when on-resize + (on-resize {:column-id id :width new-width}))))}])))] [v-box + :style (when resizable? {:position "relative"}) :children - [[h-box - :class (str "rc-simple-v-table-column-header-item " (get-in parts [:simple-column-header-item :class])) - :width (px width) - :justify justify - :align :center - :style (merge - {:padding "0px 12px" - :min-height "24px" - :height (px height) - :font-weight "bold" - :white-space "nowrap" - :overflow "hidden" - :text-overflow "ellipsis"} - (when sort-by - {:cursor "pointer"}) - (get-in parts [:simple-column-header-item :style])) - :attr (merge - (when sort-by {:on-click on-click}) - (get-in parts [:simple-column-header-item :attr])) - :children [header-label - (when sort-by - [h-box - :min-width "35px" - :justify :center - :style {:opacity 0.5} - :align :center - :children - (if-not (or @hover? current-order) - [] - [[(case current-order :asc arrow-up-icon :desc arrow-down-icon sort-icon) - {:size (or height "16px") - :fill "#777"}] - (when ps - [label :style {:visibility (when-not multiple-columns-sorted? "hidden")} :label (inc ps)])])])]]]]))) + (cond-> + [[h-box + :class (str "rc-simple-v-table-column-header-item " (get-in parts [:simple-column-header-item :class])) + :width (px width) + :justify justify + :align :center + :style (merge + {:padding "0px 12px" + :min-height "24px" + :height (px height) + :font-weight "bold" + :white-space "nowrap" + :overflow "hidden" + :text-overflow "ellipsis"} + (when sort-by + {:cursor "pointer"}) + (get-in parts [:simple-column-header-item :style])) + :attr (merge + (when sort-by {:on-click on-click}) + (get-in parts [:simple-column-header-item :attr])) + :children [header-label + (when sort-by + [h-box + :min-width "35px" + :justify :center + :style {:opacity 0.5} + :align :center + :children + (if-not (or @hover? current-order) + [] + [[(case current-order :asc arrow-up-icon :desc arrow-down-icon sort-icon) + {:size (or height "16px") + :fill "#777"}] + (when ps + [label :style {:visibility (when-not multiple-columns-sorted? "hidden")} :label (inc ps)])])])]]] + resizable? (conj [resize-handle {:parts parts :on-mouse-down start-resize}]))]))) (defn column-header-renderer ":column-header-renderer AND :top-left-renderer - Render the table header" - [{:keys [columns parts sort-by-column column-header-height hover?]}] + [{:keys [columns parts sort-by-column column-header-height hover? + resizable-columns? column-widths overlay on-resize]}] [h-box :class (str "rc-simple-v-table-column-header noselect " (get-in parts [:simple-column-header :class])) :style (merge {:padding "4px 0px" @@ -132,7 +200,8 @@ (get-in parts [:simple-column-header :attr])) :children (into [] (for [column columns] - [column-header-item {:column-header-height column-header-height :column column :parts parts :sort-by-column sort-by-column :hover? hover?}]))]) + [column-header-item {:column-header-height column-header-height :column column :parts parts :sort-by-column sort-by-column :hover? hover? + :resizable-columns? resizable-columns? :column-widths column-widths :overlay overlay :on-resize on-resize}]))]) (defn row-item "Render a single row item (column) of a single row" @@ -189,6 +258,7 @@ [{:name :simple-wrapper :level 0 :class "rc-simple-v-table-wrapper" :impl "[simple-v-table]" :notes "Outer container of the simple-v-table"} {:name :simple-column-header :level 5 :class "rc-simple-v-table-column-header" :impl "[:div]" :notes "Simple-v-table's container for column headers (placed under v-table's :column-header-content/:top-left)"} {:name :simple-column-header-item :level 6 :class "rc-simple-v-table-column-header-item" :impl "[:div]" :notes "Individual column header item/cell components"} + {:name :simple-column-header-resize-handle :level 7 :class "rc-simple-v-table-column-header-resize-handle" :impl "[:div]" :notes "Opt-in draggable grip on a column's right edge. Only present when :resizable-columns? is true (and the column's :resizable? is not false)."} {:name :simple-row :level 5 :class "rc-simple-v-table-row" :impl "[:div]" :notes "Simple-v-table's container for rows (placed under v-table's :row-content/:row-header-content)"} {:name :simple-row-item :level 6 :class "rc-simple-v-table-row-item" :impl "[:div]" :notes "Individual row item/cell components"}])) @@ -225,7 +295,10 @@ [:code ":on-export-row-label-fn"] ", " " and " [:code ":vertical-align"] ". " [:code ":sort-by"] " can be " [:code "true"] " or a map optionally containing " [:code ":key-fn"] - " and " [:code ":comp"] " ala " [:code "cljs.core/sort-by"] "."]} + " and " [:code ":comp"] " ala " [:code "cljs.core/sort-by"] ". " + "When " [:code ":resizable-columns?"] " is enabled, a column may opt out of resizing with " + [:code ":resizable? false"] ", and set a minimum drag width (px) with " [:code ":min-width"] + " (defaults to 10)."]} {:name :fixed-column-count, :required false, :default 0, @@ -347,6 +420,24 @@ :validate-fn #(or (fn? %) (map? %)), :description "Style each cell in a row either statically by passing a CSS map or dynamically by passing a function which receives the data for that row and the cell definition from the columns arg."} + {:name :resizable-columns?, + :required false, + :default false, + :type "boolean", + :description + [:span + "When true, columns can be resized by dragging a grip on their right edge; resized widths are not persisted across remounts. " + "A column can opt out with " [:code ":resizable? false"] " or set a minimum via " [:code ":min-width"] + " in its " [:code ":columns"] " entry."]} + {:name :on-resize, + :required false, + :type "{:keys [column-id width]} -> nil", + :validate-fn ifn?, + :description + [:span + "Optional callback, invoked on each step of a column-width drag with a map of " + [:code ":column-id"] " (the resized column's " [:code ":id"] ") and " [:code ":width"] + " (the new width in px). Use it to observe or persist widths. Resizing still works without it."]} {:name :class, :required false, :type "string", @@ -406,7 +497,8 @@ :show-export-button? false :table-padding 19 :table-row-line-color "#EAEEF1" - :column-header-height 31}) + :column-header-height 31 + :resizable-columns? false}) (defn simple-v-table "Render a v-table and introduce the concept of columns (provide a spec for each). @@ -421,7 +513,9 @@ (or (validate-args-macro simple-v-table-args-desc static-args) (let [sort-by-column (reagent/atom nil) - header-hover? (reagent/atom nil)] + header-hover? (reagent/atom nil) + column-widths (reagent/atom {}) ;; opt-in resizing: column :id -> width (px); empty => declared widths + overlay (reagent/atom nil)] ;; holds the drag-overlay hiccup while a column is being resized (fn simple-v-table-render [& {:as dynamic-args}] (or @@ -431,8 +525,18 @@ max-width max-rows row-height table-padding table-row-line-color on-click-row on-enter-row on-leave-row show-export-button? on-export export-button-renderer - striped? row-style class parts src debug-as] + striped? row-style class parts src debug-as + resizable-columns? on-resize] :as args} (merge default-args dynamic-args) + ;; When resizing is enabled, overlay any user-dragged widths onto the + ;; declared column widths. Threads through every downstream consumer + ;; (header items, row cells, width sums, horizontal scroll) which all + ;; read :width off the column map. + columns (if resizable-columns? + (mapv (fn [{:keys [id] :as col}] + (assoc col :width (get @column-widths id (:width col 25)))) + columns) + columns) fcc-bounded (min fixed-column-count (count columns)) fixed-cols (subvec columns 0 fcc-bounded) content-cols (subvec columns fcc-bounded (count columns)) @@ -463,12 +567,13 @@ :border-radius "3px"} (get-in parts [:simple-wrapper :style])) :attr (get-in parts [:simple-wrapper :attr]) - :child [v-table/v-table + :child [:<> + [v-table/v-table :src (at) :model model :sort-comp (multi-comparator (->v @sort-by-column)) ;; ===== Column header (section 4) - :column-header-renderer #(do [column-header-renderer (into args {:columns content-cols :hover? header-hover? :sort-by-column sort-by-column})]) + :column-header-renderer #(do [column-header-renderer (into args {:columns content-cols :hover? header-hover? :sort-by-column sort-by-column :column-widths column-widths :overlay overlay})]) :column-header-height column-header-height ;; ===== Row header (section 2) :row-header-renderer #(fn [i row] [row-renderer (into args {:columns fixed-cols :row row :row-index i})]) @@ -479,10 +584,14 @@ :max-row-viewport-height (when max-rows (* max-rows row-height)) ;:max-width (px (or max-width (+ fixed-content-width content-width u/scrollbar-tot-thick))) ; :max-width handled by enclosing parent above ;; ===== Corners (section 1, 3) - :top-left-renderer (fn [i row] [column-header-renderer {:columns fixed-cols - :hover? header-hover? - :parts parts - :sort-by-column sort-by-column}]) ;; Used when there are fixed columns + :top-left-renderer (fn [i row] [column-header-renderer {:columns fixed-cols + :hover? header-hover? + :parts parts + :sort-by-column sort-by-column + :resizable-columns? resizable-columns? + :column-widths column-widths + :overlay overlay + :on-resize on-resize}]) ;; Used when there are fixed columns :top-right-renderer (when show-export-button? #(let [rows (deref-or-value model) columns (deref-or-value columns) @@ -498,7 +607,8 @@ :cursor "default"}}} (pos? fixed-column-count) (theme/merge-props {:top-left {:style {:border-right fixed-col-border-style}} :row-headers {:style {:border-right fixed-col-border-style}}}) - :do (theme/merge-props (apply dissoc parts simple-v-table-exclusive-parts)))]])))))) + :do (theme/merge-props (apply dissoc parts simple-v-table-exclusive-parts)))] + (when @overlay @overlay)]])))))) (defn nested-column [& _] From 87ca7fd816f1224c64a68527c490dde88d81999d Mon Sep 17 00:00:00 2001 From: Rob Date: Tue, 30 Jun 2026 15:13:09 +1000 Subject: [PATCH 3/3] Tighten 2.29.4 CHANGELOG entries to the house style Drop implementation detail (Form-3 lifecycle, theme/comp timing, CSS class inventories, internal mechanics) and verbose prose, matching the terse one-line-per-change style used throughout the changelog. --- CHANGELOG.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6cfe106c..2ec3fbfd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,16 +1,16 @@ ## 2.29.4 (Unreleased) #### Added -- `simple-v-table`: opt-in column-width resizing. Pass `:resizable-columns? true` to add a draggable grip to each column's right edge; drag it to resize the column live. Backwards compatible — it defaults to `false`, so existing tables render and behave exactly as before. A column can opt out with `:resizable? false`, and set a minimum drag width (px) with `:min-width` (default 10), in its `:columns` map. An optional `:on-resize` callback receives `{:keys [column-id width]}` on each drag step so consumers can observe or persist widths. Resized widths are held internally (keyed by column `:id`) and are ephemeral (not persisted across remounts). Adds the stylable `:simple-column-header-resize-handle` part. Adapted from `nested-grid`'s resize affordance. -- `popover`: full `:parts` & `:theme` support for all five popover components (`popover-title`, `popover-border`, `popover-content-wrapper`, `popover-anchor-wrapper`, `popover-tooltip`). Closes the gap called out in the 2.29.0 changelog ("Full `:parts` & `:theme` support for most components (excluding `popover`, `v-table` and `simple-v-table`)"). The classic visual appearance is unchanged — every legacy CSS class (`rc-popover-border`, `popover`, `rc-popover-title`, `rc-popover-arrow`, `rc-popover-content`, `rc-popover-anchor-wrapper`, `rc-point-wrapper`, `rc-popover-point`, `rc-popover-tooltip`, etc.) is still applied via `bootstrap` theme methods. Form-3 lifecycle behaviour (refs, `:component-did-mount`, `:component-did-update`, position-optimization atoms) is unchanged; `theme/comp` is composed once at mount time per component instance. +- `simple-v-table`: `:resizable-columns?` adds opt-in column-width resizing — drag a grip on each column's right edge. Off by default (existing tables unaffected). Per-column `:resizable? false` opts out and `:min-width` sets a floor; optional `:on-resize` callback. Adds the `:simple-column-header-resize-handle` part. +- `popover`: full `:parts` & `:theme` support for all five popover components (`popover-title`, `popover-border`, `popover-content-wrapper`, `popover-anchor-wrapper`, `popover-tooltip`). #### Changed -- `popover-content-wrapper`: the `:parts` key for the title bar (the inner `popover-title`) is renamed `:title` → `:title-bar`. **Breaking** for any code styling the title via `:parts {:title ...}` on `popover-content-wrapper` — directly, or through `popover-anchor-wrapper`'s `:popover`: change it to `:parts {:title-bar ...}`. In dev builds the old key now raises a validation error (`Invalid keyword in :parts parameter: :title`); in production it is silently ignored. The rename disambiguates the part from the `:title` argument (the title text/hiccup), which the modern parts system would otherwise shadow. The title text, its default styling, and all CSS classes are unchanged. +- `popover-content-wrapper`: renamed the `:parts` key `:title` → `:title-bar` (disambiguates it from the `:title` text arg). **Breaking** for code styling the title via `:parts {:title ...}` — use `:parts {:title-bar ...}`. #### Fixed -- `popover-anchor-wrapper`: `:popover` argument using positional-args calling style `[popover-fn arg1 arg2 ...]` no longer breaks `:showing-injected?`/`:position-injected` injection. Previously the non-keyword branch wrapped the call as a single map, causing the receiving fn's `[a b & {:keys [...]}]` destructure to bind `a` to the entire map and produce nil kwargs — visible as a 💥 in the "Complex Popover (dialog box)" demo. Map-style invocation `[popover-fn {props}]` continues to work. [#367](https://github.com/day8/re-com/issues/367) -- `re-com.debug`: validation logger now reports `:validate-fn-return` problems with the validator's actual error message instead of an unhelpful "Unknown problem reported". This affected any validator built on `validate-arg-against-set` (e.g. `position?`, `justify-style?`, `alert-type?`) when a value didn't match the expected set — the error string was being computed but never shown. [#368](https://github.com/day8/re-com/issues/368) -- `typeahead`: async `:data-source` is no longer racey. When multiple data-source callbacks were in flight, whichever resolved last won — so a slow response for an older query could overwrite a fresh one (e.g. typing `g`, `go`, `goo` slowly enough that the `g` callback returned last left the suggestions showing matches for `g` while the input still read `goo`). Each search now carries a monotonically-increasing id; stale callbacks become no-ops. [#361](https://github.com/day8/re-com/issues/361) +- `popover`: positional-args `:popover` calling style no longer breaks the auto-injected `:showing-injected?`/`:position-injected` args. [#367](https://github.com/day8/re-com/issues/367) +- `re-com.debug`: set-based validation errors (`position?`, `alert-type?`, etc.) now report the real message instead of "Unknown problem reported". [#368](https://github.com/day8/re-com/issues/368) +- `typeahead`: async `:data-source` no longer races — a slow response for an older query can't overwrite a newer one. [#361](https://github.com/day8/re-com/issues/361) ## 2.29.3 (2026-04-29)