mirror of
https://github.com/logseq/logseq.git
synced 2026-05-27 14:14:24 +00:00
fix(icon): single-scroller layout — restore 9-column grid across all picker modes
The reaction picker and the full picker's Emojis/Icons tabs all nested two scrollbars (outer `.bd` + inner Virtuoso scroller). The pair ate ~12px of horizontal space, collapsing the 9-column grid to 8. The reaction picker additionally rendered a redundant "Emojis · N" header above its Virtuoso-Header-hosted "Recently used" section, with no visual separator and the wrong order. - pane-section: pass `:custom-scroll-parent` to Virtuoso unconditionally (was gated on `searching?`). Virtuoso defers scrolling to the nearest `.bd-scroll` ancestor in every mode, so `.bd` is the sole scrollbar. - icon.css: drop the fixed `h-[358px]` on `.pane-section.has-virtual-list`. The pane grows to Virtuoso's reported list height; `.bd-scroll` catches the overflow. - emojis-cp: render "Recently used" and "Emojis" as sibling pane-sections (same shape as `all-pane`) instead of nesting recents in Virtuoso's Header slot. Fixes header ordering and gives the two sections a natural divider (the Emojis section-header). - Reaction-picker call sites (block.cljs, comments.cljs, content.cljs, ui.cljs): replace the inline emoji-only opts with `(merge reaction-picker-opts ...)` for consistency. Verified live across every surface (All / Emojis / Icons / Custom / search / reaction picker): exactly one scrollbar, 9 columns, correct header order. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -2861,14 +2861,8 @@
|
||||
(.-target e)
|
||||
(fn [{:keys [id]}]
|
||||
(icon-component/icon-search
|
||||
{:on-chosen (fn [_emoji-event emoji _keep-popup?] (on-pick id emoji))
|
||||
;; Reaction picker is emoji-only and minimal —
|
||||
;; no tabs/color/trash chrome.
|
||||
:allowed-tabs [:emoji]
|
||||
:default-tab :emoji
|
||||
:hide-topbar? true
|
||||
:show-used? true
|
||||
:icon-value nil}))
|
||||
(merge icon-component/reaction-picker-opts
|
||||
{:on-chosen (fn [_emoji-event emoji _keep-popup?] (on-pick id emoji))})))
|
||||
{:align :start
|
||||
:content-props {:class "ls-icon-picker"}}))]
|
||||
(when (seq summary)
|
||||
|
||||
@@ -271,16 +271,10 @@
|
||||
target
|
||||
(fn [{:keys [id]}]
|
||||
(icon-component/icon-search
|
||||
{:on-chosen (fn [_emoji-event emoji _keep-popup?]
|
||||
(reaction-handler/toggle-reaction! (:block/uuid comment-block) (:id emoji))
|
||||
(shui/popup-hide! id))
|
||||
;; Reaction picker is emoji-only and minimal — no tabs/color/trash
|
||||
;; chrome, just search + emoji grid.
|
||||
:allowed-tabs [:emoji]
|
||||
:default-tab :emoji
|
||||
:hide-topbar? true
|
||||
:show-used? true
|
||||
:icon-value nil}))
|
||||
(merge icon-component/reaction-picker-opts
|
||||
{:on-chosen (fn [_emoji-event emoji _keep-popup?]
|
||||
(reaction-handler/toggle-reaction! (:block/uuid comment-block) (:id emoji))
|
||||
(shui/popup-hide! id))})))
|
||||
{:align :end
|
||||
:content-props {:class "ls-icon-picker"}})))
|
||||
|
||||
|
||||
@@ -219,22 +219,16 @@
|
||||
{:class "!p-0"}
|
||||
[:div.p-1
|
||||
(icon-component/icon-search
|
||||
{:on-chosen (fn [_e icon]
|
||||
(let [emoji-id (:id icon)
|
||||
emoji? (= :emoji (:type icon))]
|
||||
(if emoji?
|
||||
(do
|
||||
(reaction-handler/toggle-reaction! block-id emoji-id)
|
||||
(state/hide-custom-context-menu!)
|
||||
(shui/popup-hide!))
|
||||
(notification/show! (t :block.reaction/emoji-required-warning) :warning))))
|
||||
;; Reaction picker is emoji-only and minimal — no tabs/color/trash
|
||||
;; chrome, just search + emoji grid.
|
||||
:allowed-tabs [:emoji]
|
||||
:default-tab :emoji
|
||||
:hide-topbar? true
|
||||
:show-used? true
|
||||
:icon-value nil})]))
|
||||
(merge icon-component/reaction-picker-opts
|
||||
{:on-chosen (fn [_e icon _keep-popup?]
|
||||
(let [emoji-id (:id icon)
|
||||
emoji? (= :emoji (:type icon))]
|
||||
(if emoji?
|
||||
(do
|
||||
(reaction-handler/toggle-reaction! block-id emoji-id)
|
||||
(state/hide-custom-context-menu!)
|
||||
(shui/popup-hide!))
|
||||
(notification/show! (t :block.reaction/emoji-required-warning) :warning))))}))]))
|
||||
|
||||
(shui/dropdown-menu-sub
|
||||
{:open set-icon-sub-menu-open?
|
||||
|
||||
@@ -2186,7 +2186,7 @@
|
||||
(shui/shortcut keyboard-hint {:style :compact})]))])
|
||||
|
||||
(rum/defc pane-section
|
||||
[label icon-items & {:keys [collapsible? keyboard-hint total-count searching? virtual-list? render-item-fn expanded? focus-region show-header? *virtuoso-ref]
|
||||
[label icon-items & {:keys [collapsible? keyboard-hint total-count searching? virtual-list? render-item-fn expanded? focus-region show-header? *virtuoso-ref header-cp]
|
||||
:or {virtual-list? true collapsible? false expanded? true show-header? true}
|
||||
:as opts}]
|
||||
(let [*el-ref (rum/use-ref nil)
|
||||
@@ -2223,6 +2223,18 @@
|
||||
:ref (fn [^js el]
|
||||
(when *virtuoso-ref
|
||||
(reset! *virtuoso-ref el)))
|
||||
;; Single-scroller layout: Virtuoso delegates
|
||||
;; scrolling to the nearest `.bd-scroll` ancestor
|
||||
;; instead of creating its own internal scroller.
|
||||
;; This keeps `.bd` as the only scroll surface
|
||||
;; across every picker mode (All / Emojis / Icons /
|
||||
;; reaction / search), reclaiming the ~6px the
|
||||
;; inner Virtuoso scrollbar would otherwise eat so
|
||||
;; the 9-column grid stays at 9. On first render
|
||||
;; the ref isn't attached yet and this is `nil`;
|
||||
;; Virtuoso falls back to internal scrolling for
|
||||
;; one frame, then re-renders with the parent.
|
||||
:custom-scroll-parent (some-> (rum/deref *el-ref) (.closest ".bd-scroll"))
|
||||
:item-content (fn [idx]
|
||||
(icons-row
|
||||
(let [last? (= (dec rows) idx)
|
||||
@@ -2238,55 +2250,48 @@
|
||||
(render-fn item (assoc opts :wave {:r idx :c c-idx})))
|
||||
icons)))))}
|
||||
|
||||
searching?
|
||||
(assoc :custom-scroll-parent (some-> (rum/deref *el-ref) (.closest ".bd-scroll"))))))
|
||||
header-cp
|
||||
(assoc :components #js {:Header header-cp}))))
|
||||
[:div.its
|
||||
(map-indexed
|
||||
(fn [i item]
|
||||
(render-fn item (assoc opts :wave {:r (quot i icon-grid-cols) :c (mod i icon-grid-cols)})))
|
||||
icon-items)]))]))
|
||||
|
||||
(defn- normalize-tabs
|
||||
[tabs default-tab]
|
||||
(let [tabs (or tabs [[:all (t :icon/tab-all)]
|
||||
[:emoji (t :icon/tab-emojis)]
|
||||
[:icon (t :icon/tab-icons)]])
|
||||
default-tab (or default-tab (ffirst tabs) :all)
|
||||
default-tab (if (some #(= (first %) default-tab) tabs)
|
||||
default-tab
|
||||
(ffirst tabs))]
|
||||
{:tabs tabs
|
||||
:default-tab default-tab
|
||||
:has-icon-tab? (boolean (some #(= (first %) :icon) tabs))}))
|
||||
(def reaction-picker-opts
|
||||
"Standard opts for the minimal emoji-only reaction picker. Callers
|
||||
`merge` their own `:on-chosen` (and any additional opts) onto this."
|
||||
{:allowed-tabs [:emoji]
|
||||
:hide-topbar? true
|
||||
:show-used? true
|
||||
:icon-value nil})
|
||||
|
||||
(defn- emoji-sections
|
||||
[emojis* used-items show-used?]
|
||||
(let [emoji-used-items (when (seq used-items)
|
||||
(filterv #(= :emoji (:type %)) used-items))
|
||||
sections (cond-> []
|
||||
(and show-used? (seq emoji-used-items))
|
||||
(conj {:title (t :ui/frequently-used)
|
||||
:items emoji-used-items
|
||||
:virtual-list? false})
|
||||
true
|
||||
(conj {:title (t :icon/emojis-count (count emojis*))
|
||||
:items emojis*
|
||||
:virtual-list? true}))]
|
||||
sections))
|
||||
|
||||
;; Note: `get-used-items` and `add-used-item!` are defined further down
|
||||
;; (~line 2194) with v2-storage migration + type-aware dedup + renderable
|
||||
;; filtering — preferred over master's simpler legacy-format versions.
|
||||
(declare get-used-items)
|
||||
|
||||
(rum/defc emojis-cp < rum/static
|
||||
[emojis* opts]
|
||||
(let [icon-items (map (fn [emoji]
|
||||
[emojis* {:keys [show-used?] :as opts}]
|
||||
(let [used-emojis (when show-used?
|
||||
(->> (get-used-items)
|
||||
(filterv #(= :emoji (:type %)))))
|
||||
has-recents? (seq used-emojis)
|
||||
icon-items (map (fn [emoji]
|
||||
{:type :emoji
|
||||
:id (:id emoji)
|
||||
:label (or (:name emoji) (:id emoji))
|
||||
:data {:value (:id emoji)}})
|
||||
emojis*)]
|
||||
(pane-section "Emojis" icon-items (assoc opts :show-header? false))))
|
||||
;; Recents render as a sibling pane-section above the full grid.
|
||||
;; Single scroll surface (.bd) means a sibling no longer triggers
|
||||
;; a second scrollbar — same compositional pattern as `all-pane`.
|
||||
;; The Emojis header doubles as the visual divider between the two
|
||||
;; sections; suppress it when there are no recents (full picker
|
||||
;; Emojis tab) to keep the picker minimal there.
|
||||
[:<>
|
||||
(when has-recents?
|
||||
(pane-section "Recently used" used-emojis
|
||||
(assoc opts :virtual-list? false)))
|
||||
(pane-section "Emojis" icon-items
|
||||
(assoc opts :show-header? has-recents?))]))
|
||||
|
||||
(rum/defc icons-cp < rum/static
|
||||
[icons opts]
|
||||
@@ -4991,7 +4996,7 @@
|
||||
(defn- compute-flat-items
|
||||
"Compute the flat navigable item list and section metadata for the current view.
|
||||
Returns {:items [icon-item ...] :sections [{:start N :count N :cols N} ...]}."
|
||||
[tab result section-states]
|
||||
[tab result section-states & [{:keys [show-used?]}]]
|
||||
(let [build-sections (fn [& groups]
|
||||
(loop [gs groups offset 0 items [] sections []]
|
||||
(if-let [g (first gs)]
|
||||
@@ -5071,14 +5076,24 @@
|
||||
:label icon-name :data {:value icon-name}}))))
|
||||
:cols icon-grid-cols})
|
||||
|
||||
;; Emojis tab: full emoji list
|
||||
;; Emojis tab: full emoji list, optionally preceded by recently-used
|
||||
;; emojis when :show-used? is true (reaction-picker context).
|
||||
(= tab :emoji)
|
||||
(let [items (vec (map (fn [emoji]
|
||||
{:type :emoji :id (:id emoji)
|
||||
:label (or (:name emoji) (:id emoji))
|
||||
:data {:value (:id emoji)}})
|
||||
emojis))]
|
||||
{:items items :sections [{:start 0 :count (count items) :cols icon-grid-cols}]})
|
||||
(build-sections
|
||||
(when show-used?
|
||||
{:label "Recently used"
|
||||
:items (when (get section-states "Recently used" true)
|
||||
(->> (get-used-items)
|
||||
(filterv #(= :emoji (:type %)))))
|
||||
:cols icon-grid-cols})
|
||||
{:label "Emojis"
|
||||
:items (when (get section-states "Emojis" true)
|
||||
(mapv (fn [emoji]
|
||||
{:type :emoji :id (:id emoji)
|
||||
:label (or (:name emoji) (:id emoji))
|
||||
:data {:value (:id emoji)}})
|
||||
emojis))
|
||||
:cols icon-grid-cols})
|
||||
|
||||
;; Icons tab: full icon list
|
||||
(= tab :icon)
|
||||
@@ -7020,7 +7035,8 @@
|
||||
;; reads can stay as bare derefs once the component is hooked up.
|
||||
highlighted-idx (rum/react *highlighted-index)
|
||||
section-states @*section-states
|
||||
{flat-items :items sections :sections} (compute-flat-items @*tab result section-states)
|
||||
{flat-items :items sections :sections} (compute-flat-items @*tab result section-states
|
||||
{:show-used? (:show-used? opts)})
|
||||
highlighted-id (when-let [idx highlighted-idx]
|
||||
(when (< idx (count flat-items))
|
||||
(:id (nth flat-items idx))))
|
||||
|
||||
@@ -158,11 +158,12 @@
|
||||
}
|
||||
|
||||
&.has-virtual-list {
|
||||
@apply h-[358px] overflow-y-visible;
|
||||
|
||||
&.searching-result {
|
||||
@apply h-auto;
|
||||
}
|
||||
/* Single-scroller layout: pane-section grows to Virtuoso's
|
||||
reported list height; the outer `.bd-scroll` is the only
|
||||
scroll surface (Virtuoso uses `:custom-scroll-parent` to
|
||||
defer scrolling to it). The class itself stays as a marker
|
||||
hook for other rules / data attributes. */
|
||||
@apply h-auto;
|
||||
}
|
||||
|
||||
.virtuoso-item-list {
|
||||
|
||||
@@ -278,14 +278,8 @@
|
||||
target'
|
||||
(fn [{:keys [id]}]
|
||||
(icon-component/icon-search
|
||||
{:on-chosen (fn [_e icon _keep-popup?] (on-pick id icon))
|
||||
;; Reaction picker is emoji-only and minimal — no tabs/color/trash
|
||||
;; chrome, just search + emoji grid.
|
||||
:allowed-tabs [:emoji]
|
||||
:default-tab :emoji
|
||||
:hide-topbar? true
|
||||
:show-used? true
|
||||
:icon-value nil}))
|
||||
(merge icon-component/reaction-picker-opts
|
||||
{:on-chosen (fn [_e icon _keep-popup?] (on-pick id icon))})))
|
||||
{:align :start
|
||||
:content-props {:class "ls-icon-picker"}}))))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user