New component - URL Preview List (#15620)
Implements the URL Preview List component. Fixes https://github.com/status-im/status-mobile/issues/15617 Notes ===== The quo component view implements the pattern recently described by @ulisesmac. See his great explanation here: https://github.com/status-im/status-mobile/issues/15552#issuecomment-1492590074. Steps to test: Go to `Quo2.0 Preview` > `links` > `url-preview-list`
This commit is contained in:
parent
270f5ab258
commit
d902fb10d1
|
@ -0,0 +1,49 @@
|
||||||
|
(ns quo2.components.links.url-preview-list.component-spec
|
||||||
|
(:require
|
||||||
|
[oops.core :as oops]
|
||||||
|
[quo2.components.links.url-preview-list.view :as view]
|
||||||
|
[test-helpers.component :as h]))
|
||||||
|
|
||||||
|
(def previews
|
||||||
|
(->> (range 3)
|
||||||
|
(map inc)
|
||||||
|
(mapv (fn [index]
|
||||||
|
{:title (str "Title " index)
|
||||||
|
:body (str "status.im." index)
|
||||||
|
:loading? false
|
||||||
|
:url (str "status.im." index)}))))
|
||||||
|
|
||||||
|
(h/describe "Links - URL Preview List"
|
||||||
|
(h/test "default render"
|
||||||
|
(h/render [view/view
|
||||||
|
{:data previews
|
||||||
|
:key-fn :url
|
||||||
|
:horizontal-spacing 10}])
|
||||||
|
(-> (count (h/query-all-by-label-text :url-preview))
|
||||||
|
(h/expect)
|
||||||
|
(.toEqual 3))
|
||||||
|
|
||||||
|
(-> (map #(oops/oget % "props.children")
|
||||||
|
(h/query-all-by-label-text :title))
|
||||||
|
(clj->js)
|
||||||
|
(h/expect)
|
||||||
|
(.toStrictEqual #js ["Title 1" "Title 2" "Title 3"])))
|
||||||
|
|
||||||
|
(h/test "on-clear event is individually handled by each preview"
|
||||||
|
(let [on-clear (h/mock-fn)]
|
||||||
|
(h/render [view/view
|
||||||
|
{:data previews
|
||||||
|
:key-fn :url
|
||||||
|
:on-clear on-clear}])
|
||||||
|
(h/fire-event :press (first (h/get-all-by-label-text :button-clear-preview)))
|
||||||
|
(h/fire-event :press (second (h/get-all-by-label-text :button-clear-preview)))
|
||||||
|
(h/was-called-times on-clear 2)))
|
||||||
|
|
||||||
|
(h/test "previews have separate loading states"
|
||||||
|
(h/render [view/view
|
||||||
|
{:data (assoc-in previews [1 :loading?] true)
|
||||||
|
:key-fn :url}])
|
||||||
|
(h/is-truthy (h/get-by-label-text :url-preview-loading))
|
||||||
|
(-> (count (h/query-all-by-label-text :url-preview))
|
||||||
|
(h/expect)
|
||||||
|
(.toEqual 2))))
|
|
@ -0,0 +1,8 @@
|
||||||
|
(ns quo2.components.links.url-preview-list.style)
|
||||||
|
|
||||||
|
(def url-preview-gap 12)
|
||||||
|
|
||||||
|
(def url-preview-separator
|
||||||
|
{:width url-preview-gap
|
||||||
|
:padding-top 12
|
||||||
|
:padding-bottom 8})
|
|
@ -0,0 +1,86 @@
|
||||||
|
(ns quo2.components.links.url-preview-list.view
|
||||||
|
(:require
|
||||||
|
[oops.core :as oops]
|
||||||
|
[quo2.components.links.url-preview-list.style :as style]
|
||||||
|
[quo2.components.links.url-preview.view :as url-preview]
|
||||||
|
[react-native.core :as rn]
|
||||||
|
[reagent.core :as reagent]))
|
||||||
|
|
||||||
|
(defn- use-scroll-to-last-item
|
||||||
|
[flat-list-ref item-count item-width]
|
||||||
|
(rn/use-effect
|
||||||
|
(fn []
|
||||||
|
(when (and (pos? item-count) (pos? item-width))
|
||||||
|
;; We use a delay because calling `scrollToOffset` without a delay does
|
||||||
|
;; nothing while the flatlist is still rendering its children.
|
||||||
|
;; `scrollToEnd` doesn't work because it positions the item off-center
|
||||||
|
;; and there's no argument to offset it.
|
||||||
|
(let [timer-id (js/setTimeout
|
||||||
|
(fn []
|
||||||
|
(when (and @flat-list-ref (pos? item-count))
|
||||||
|
(.scrollToOffset ^js @flat-list-ref
|
||||||
|
#js
|
||||||
|
{:animated true
|
||||||
|
:offset (* (+ item-width style/url-preview-gap)
|
||||||
|
(max 0 (dec item-count)))})))
|
||||||
|
25)]
|
||||||
|
(fn []
|
||||||
|
(js/clearTimeout timer-id)))))
|
||||||
|
[item-count item-width]))
|
||||||
|
|
||||||
|
(defn- separator
|
||||||
|
[]
|
||||||
|
[rn/view {:style style/url-preview-separator}])
|
||||||
|
|
||||||
|
(defn- item-component
|
||||||
|
[{:keys [title body loading? logo]} _ _
|
||||||
|
{:keys [width on-clear loading-message container-style]}]
|
||||||
|
[url-preview/view
|
||||||
|
{:logo logo
|
||||||
|
:title title
|
||||||
|
:body body
|
||||||
|
:loading? loading?
|
||||||
|
:loading-message loading-message
|
||||||
|
:on-clear on-clear
|
||||||
|
:container-style (merge container-style {:width width})}])
|
||||||
|
|
||||||
|
(defn- calculate-width
|
||||||
|
[preview-width horizontal-spacing ^js e]
|
||||||
|
(reset! preview-width
|
||||||
|
(- (oops/oget e "nativeEvent.layout.width")
|
||||||
|
(* 2 horizontal-spacing))))
|
||||||
|
|
||||||
|
(defn- view-component
|
||||||
|
[]
|
||||||
|
(let [preview-width (reagent/atom 0)
|
||||||
|
flat-list-ref (atom nil)]
|
||||||
|
(fn [{:keys [data key-fn horizontal-spacing on-clear loading-message
|
||||||
|
container-style container-style-item]}]
|
||||||
|
(use-scroll-to-last-item flat-list-ref (count data) @preview-width)
|
||||||
|
;; We need to use a wrapping view expanded to 100% instead of "flex 1",
|
||||||
|
;; otherwise `on-layout` will be triggered multiple times as the flat list
|
||||||
|
;; renders its children.
|
||||||
|
[rn/view
|
||||||
|
{:style (merge container-style {:width "100%"})
|
||||||
|
:accessibility-label :url-preview-list}
|
||||||
|
[rn/flat-list
|
||||||
|
{:ref #(reset! flat-list-ref %)
|
||||||
|
:key-fn key-fn
|
||||||
|
:on-layout #(calculate-width preview-width horizontal-spacing %)
|
||||||
|
:horizontal true
|
||||||
|
:deceleration-rate :fast
|
||||||
|
:on-scroll-to-index-failed identity
|
||||||
|
:content-container-style {:padding-horizontal horizontal-spacing}
|
||||||
|
:separator [separator]
|
||||||
|
:snap-to-interval (+ @preview-width style/url-preview-gap)
|
||||||
|
:shows-horizontal-scroll-indicator false
|
||||||
|
:data data
|
||||||
|
:render-fn item-component
|
||||||
|
:render-data {:width @preview-width
|
||||||
|
:on-clear on-clear
|
||||||
|
:loading-message loading-message
|
||||||
|
:container-style container-style-item}}]])))
|
||||||
|
|
||||||
|
(defn view
|
||||||
|
[props]
|
||||||
|
[:f> view-component props])
|
|
@ -34,6 +34,7 @@
|
||||||
quo2.components.inputs.title-input.view
|
quo2.components.inputs.title-input.view
|
||||||
quo2.components.inputs.profile-input.view
|
quo2.components.inputs.profile-input.view
|
||||||
quo2.components.links.url-preview.view
|
quo2.components.links.url-preview.view
|
||||||
|
quo2.components.links.url-preview-list.view
|
||||||
quo2.components.list-items.channel
|
quo2.components.list-items.channel
|
||||||
quo2.components.list-items.menu-item
|
quo2.components.list-items.menu-item
|
||||||
quo2.components.list-items.preview-list
|
quo2.components.list-items.preview-list
|
||||||
|
@ -199,3 +200,4 @@
|
||||||
|
|
||||||
;;;; LINKS
|
;;;; LINKS
|
||||||
(def url-preview quo2.components.links.url-preview.view/view)
|
(def url-preview quo2.components.links.url-preview.view/view)
|
||||||
|
(def url-preview-list quo2.components.links.url-preview-list.view/view)
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
[quo2.components.inputs.input.component-spec]
|
[quo2.components.inputs.input.component-spec]
|
||||||
[quo2.components.inputs.profile-input.component-spec]
|
[quo2.components.inputs.profile-input.component-spec]
|
||||||
[quo2.components.inputs.title-input.component-spec]
|
[quo2.components.inputs.title-input.component-spec]
|
||||||
|
[quo2.components.links.url-preview-list.component-spec]
|
||||||
[quo2.components.links.url-preview.component-spec]
|
[quo2.components.links.url-preview.component-spec]
|
||||||
[quo2.components.markdown.--tests--.text-component-spec]
|
[quo2.components.markdown.--tests--.text-component-spec]
|
||||||
[quo2.components.onboarding.small-option-card.component-spec]
|
[quo2.components.onboarding.small-option-card.component-spec]
|
||||||
|
|
|
@ -0,0 +1,47 @@
|
||||||
|
(ns status-im2.contexts.quo-preview.links.url-preview-list
|
||||||
|
(:require
|
||||||
|
[quo2.core :as quo]
|
||||||
|
[quo2.foundations.colors :as colors]
|
||||||
|
[react-native.core :as rn]
|
||||||
|
[reagent.core :as reagent]
|
||||||
|
[status-im2.common.resources :as resources]
|
||||||
|
[status-im2.contexts.quo-preview.preview :as preview]
|
||||||
|
utils.number))
|
||||||
|
|
||||||
|
(def descriptor
|
||||||
|
[{:label "Number of previews"
|
||||||
|
:key :previews-length
|
||||||
|
:type :text}])
|
||||||
|
|
||||||
|
(defn cool-preview
|
||||||
|
[]
|
||||||
|
(let [state (reagent/atom {:previews-length "3"})]
|
||||||
|
(fn []
|
||||||
|
(let [previews-length (min 6 (utils.number/parse-int (:previews-length @state)))]
|
||||||
|
[rn/view {:style {:padding-bottom 150}}
|
||||||
|
[preview/customizer state descriptor]
|
||||||
|
[rn/view
|
||||||
|
{:style {:align-items :center
|
||||||
|
:margin-top 50}}
|
||||||
|
[quo/url-preview-list
|
||||||
|
{:horizontal-spacing 20
|
||||||
|
:on-clear #(js/alert "Clear button pressed")
|
||||||
|
:key-fn :url
|
||||||
|
:data (for [index (range previews-length)
|
||||||
|
:let [index (inc index)]]
|
||||||
|
{:title (str "Title " index)
|
||||||
|
:body (str "status.im." index)
|
||||||
|
:logo (resources/get-mock-image :status-logo)
|
||||||
|
:loading? false
|
||||||
|
:url (str "status.im." index)})}]]]))))
|
||||||
|
|
||||||
|
(defn preview
|
||||||
|
[]
|
||||||
|
[rn/view
|
||||||
|
{:style {:background-color (colors/theme-colors colors/white colors/neutral-95)
|
||||||
|
:flex 1}}
|
||||||
|
[rn/flat-list
|
||||||
|
{:flex 1
|
||||||
|
:keyboard-should-persist-taps :always
|
||||||
|
:header [cool-preview]
|
||||||
|
:key-fn str}]])
|
|
@ -40,6 +40,7 @@
|
||||||
[status-im2.contexts.quo-preview.inputs.profile-input :as profile-input]
|
[status-im2.contexts.quo-preview.inputs.profile-input :as profile-input]
|
||||||
[status-im2.contexts.quo-preview.inputs.title-input :as title-input]
|
[status-im2.contexts.quo-preview.inputs.title-input :as title-input]
|
||||||
[status-im2.contexts.quo-preview.links.url-preview :as url-preview]
|
[status-im2.contexts.quo-preview.links.url-preview :as url-preview]
|
||||||
|
[status-im2.contexts.quo-preview.links.url-preview-list :as url-preview-list]
|
||||||
[status-im2.contexts.quo-preview.list-items.channel :as channel]
|
[status-im2.contexts.quo-preview.list-items.channel :as channel]
|
||||||
[status-im2.contexts.quo-preview.list-items.preview-lists :as preview-lists]
|
[status-im2.contexts.quo-preview.list-items.preview-lists :as preview-lists]
|
||||||
[status-im2.contexts.quo-preview.markdown.text :as text]
|
[status-im2.contexts.quo-preview.markdown.text :as text]
|
||||||
|
@ -183,7 +184,10 @@
|
||||||
:component title-input/preview-title-input}]
|
:component title-input/preview-title-input}]
|
||||||
:links [{:name :url-preview
|
:links [{:name :url-preview
|
||||||
:options {:insets {:top? true}}
|
:options {:insets {:top? true}}
|
||||||
:component url-preview/preview}]
|
:component url-preview/preview}
|
||||||
|
{:name :url-preview-list
|
||||||
|
:options {:insets {:top? true}}
|
||||||
|
:component url-preview-list/preview}]
|
||||||
:list-items [{:name :channel
|
:list-items [{:name :channel
|
||||||
:insets {:top false}
|
:insets {:top false}
|
||||||
:component channel/preview-channel}
|
:component channel/preview-channel}
|
||||||
|
|
|
@ -61,6 +61,8 @@
|
||||||
"Pretty-print to STDOUT the current component tree."
|
"Pretty-print to STDOUT the current component tree."
|
||||||
(with-node-or-screen :debug))
|
(with-node-or-screen :debug))
|
||||||
|
|
||||||
|
(def within rtl/within)
|
||||||
|
|
||||||
(defn fire-event
|
(defn fire-event
|
||||||
([event-name node]
|
([event-name node]
|
||||||
(fire-event event-name node nil))
|
(fire-event event-name node nil))
|
||||||
|
@ -188,3 +190,7 @@
|
||||||
(defn was-called
|
(defn was-called
|
||||||
[mock]
|
[mock]
|
||||||
(.toHaveBeenCalled (js/expect mock)))
|
(.toHaveBeenCalled (js/expect mock)))
|
||||||
|
|
||||||
|
(defn was-called-times
|
||||||
|
[^js mock number-of-times]
|
||||||
|
(.toHaveBeenCalledTimes (js/expect mock) number-of-times))
|
||||||
|
|
Loading…
Reference in New Issue