status-mobile/src/quo
mmilad75 d223151b93
Implement Alert Banner Component #20277 (#20278)
2024-06-07 18:56:14 +02:00
..
components Implement Alert Banner Component #20277 (#20278) 2024-06-07 18:56:14 +02:00
foundations Feature: add wallet network settings to app settings (#20254) 2024-06-05 12:17:14 +01:00
README.md Added destructuring section to new-guidelines.md (#18731) 2024-03-04 11:38:50 -03:00
core.cljs Implement Alert Banner Component #20277 (#20278) 2024-06-07 18:56:14 +02:00
core_spec.cljs Implement Alert Banner Component #20277 (#20278) 2024-06-07 18:56:14 +02:00
theme.cljs Feature/remove reagent atom from theme (#19706) 2024-04-18 16:14:17 +02:00

README.md

Quo Library

Quo is the name of our mobile component library that implements the Status Design System for Mobile.

The overarching goals of having this component library are:

  • Achieve the highest possible fidelity between code and the design system in Figma.
  • Decouple components from ever-changing business requirements.

[!NOTE] This document captures our current practices and guidelines for implementing the design system. For guidelines that apply across the entire project, take a look at new-guidelines.

Directory structure and file names

We follow one basic rule: mirror Figma pages and their component names in the directory structure.

For example, in the screenshot below we see the Figma page is Banners and the component name is Banner.

Therefore, the structure should look like:

quo/
└── components/
    └── banners/
        └── banner/
            ├── component_spec.cljs
            ├── style.cljs
            └── view.cljs

Files view.cljs, style.cljs, and component_spec.cljs should always have the same name, regardless of component.

Component API

Adhere to the same component properties and values used in a Figma component when translating it to Clojure. This means using the same names for props and the same values. If the Figma property is a boolean, use a question mark suffix to make the name more idiomatic in Clojure.

We have found over time that the less we drift from the design system the better. Some key benefits:

  • It helps developers quickly check for issues when comparing the code with the source of truth in Figma.
  • It is easier for pull-request reviewers to double-check components for correctness.
  • It helps developers create preview screens that are identical or very similar to Figma, which aids in spotting bugs more easily.
  • It helps designers review all component variations in preview screens.

In the image above we can see the properties are Type, State, Size, Icon, Theme, and Background. Translated to Clojure:

;; ns quo.components.buttons.button.view
(def view
  [{:keys [type state size icon theme background]}]
  ...)

Handling Sizes

In the designs, sizes are referred to as integers. To avoid having the codebase littered with magic numbers we instead have a keyword convention to use in components to map these keywords with their sizes.

The convention is :size-<number>, e.g size 20 is :size-20

;; bad
(defn button
  [{:keys [size]}]
  [rn/view
   {:style {:height (case size
                      20 20
                      40 40
                      0)}}]
  ...)
;; good
(defn button
  [{:keys [size]}]
  [rn/view
   {:style {:height (case size
                      :size-20 20
                      :size-40 40
                      0)}}]
  ...)

Clojure var conventions

  • Due to the fact that every view namespace should export only one component and to avoid the redundancy of [some-component/some-component ...], name the public var view as well.
  • Try to make all other vars private because they should almost never be used directly.

Default value when destructuring

Too often callers pass nil values because values can be wrapped in a when for example. In this case, the default value is not applied, because :or macro will use default only when the value is absent. Instead, use (or (:value props) some-default-value) in a let expression or as a parameter value.

;; bad (unreliable)
(defn- view-internal
  [{:keys [auto-focus?
           init-value
           return-key-type]
    :or   {auto-focus?     false
           init-value      0
           return-key-type :done}}]
  ...)

;; good
(defn- view-internal
   [{:keys [theme size something] :as props}]
   (let [auto-focus? (or (:auto-focus? props) false)
         init-value (or (:init-value props) 0)
         return-key-type (or (:return-key-type props) :done)]
     ...))

Component tests

We don't attempt to write component tests verifying how components look on the screen. Instead, we have found a middle ground, where the focus is on verifying if events are triggered as intended and that all component variations are rendered. We use React Native Testing Library.

There are dozens of examples in the repository, so use them as a reference. A good and complete example is quo.components.avatars.user-avatar.component-spec

No-props test

When writing tests for the component that has props, please add one test that covers situation when props aren't passed. Because even if component not showing anything meaningful without props, it shouldn't crash.

(h/describe "Transaction Progress"
  (h/test "component renders without props"
    (h/render [quo/transaction-progress {}])
    (h/is-truthy (h/get-by-label-text :transaction-progress)))

Do not couple the library with re-frame

Don't use re-frame inside this library (e.g. dispatch & subscribe). If a component needs to be stateful, the state should be local to its rendering lifecycle (using reagent.core/atom). Additionally, if the component requires any other data, it should be passed as arguments.

;; bad
(defn view []
  (let [window-width (rf/sub [:dimensions/window-width])]
    [rn/pressable {:on-press #(rf/dispatch [:do-xyz])}
     (do-something window-width)]))

;; good
(defn view [{:keys [window-width on-press]}]
  [rn/pressable {:on-press on-press}
   (do-something window-width)])

Themes

Our goal is to make all design system components themeable, which means they should not use, nor fallback to the OS theme, because themes are contextual and can be overridden in specific parts of the app.

To achieve this, use the higher-order function quo.theme/with-theme to automatically inject the current theme context (based on the React Context API).

Use the following pattern:

(ns quo.components.<figma page>.<component name>.view
  (:require [quo.theme :as quo.theme]))

(defn- view-internal [{:keys [theme]}]
  ...)

(def view (quo.theme/with-theme view-internal))

Then pass the theme value down to all functions that may rely on the OS theme, like quo.foundations.colors/theme-colors or quo.foundations.shadows/get.

Avoid using quo's version number in namespace aliases

When requiring quo namespaces, don't use the version number in the alias, unless for a special reason you need to require both the old and new namespaces in the same file.

[!NOTE] Keep in mind that, at the moment, we need to keep both src/quo/ and src/quo/ directories in the repository, but eventually the old one will go away and the version number will lose its meaning.

;; bad
(ns ...
  (require [quo.theme :as quo.theme]
           [quo.core :as quo]))

;; good
(ns ...
  (require [quo.theme :as quo.theme]
           [quo.core :as quo]))

Preview screens

Every component should be accompanied by a preview screen in src/status_im/contexts/quo_preview/. Ideally, all possible variations in Figma should be achievable in the preview screen by changing the input values without resorting to code changes. Designers will also use this capability to review components in PR builds.

Allow outermost containers to have their styles overridden

If a component needs to be wrapped in a rn/view instance to force it to be styled differently, consider changing the component to accept a container-style argument. This will help reduce the number of nodes to be rendered.

;; bad
[rn/view {:style {:margin-right 12}}
 [quo/button
  {:size 32}
  :i/info]]

;; good
[quo/button
 {:size            32
  :container-style {:margin-right 12}}
 :i/info]