# 4. Better Layout Primitives

Date: 2020-07-24

## Status

Implemented

## Context

Patchwork currently has two mechanisms to help with layout: the `Grid`/`GridItem` components, which are designed for page-level layout, and the `spacing`/`stacking` mixins, which are for lower-level layout in places where `Grid` is not appropriate.

Since `Grid` is intended for use at the page level, it does not work well in nested scenarios. This leaves a gap where we often need a way to arrange sets of form controls or other components in complex relationships together and still have them behave responsively. Current solutions to this generally involve a combination of custom CSS and "`Box`-soup", which is undesirable.

This ADR proposes some extensions to the `spacing`/`stacking` mixins that together provide a more robust layout approach.

The main gaps in the current implementation I've observed are

1. **Relative Sizing of Children in a Stack:** Inability to control the relative sizes of things laid out in a stack (e.g. several form controls in the same row having arbitrary widths)
2. **Control over Responsive Reflow:** Inflexibility when it comes to reflowing for different breakpoints.
3. **Layout-Only Boxes:** Not enough component types supporting the `spacing`/`stacking` mixins, leading to lots of extra `Box`-es just for layout purposes.

## Principles

**Backwards compatibility:** We have a lot of usage of `spacing` and `stacking` in the wild. It would be nice not to break this.

**Low cognitive overhead:** I've tried to do three things to reduce the cognitive overhead associated with the proposed change:

1. extend the existing `stacking`/`spacing` mechanism instead of introducing a second, independent approach. This is to avoid confusion where people don't know which approach is best for a given situation.

2. incremental adoption: where we need to supply additional properties for new behaviour, I have structured the props in such a way that you only have to supply extra properties if you actually need that feature.

3. similarity to ReactNative: one of patchwork's goals is to make it easier to hop between our web and mobile codebases. Ideally Patchwork web should hide DOM/CSS details just as Patchwork mobile hides Dalvik/Cocoa details. ReactNative's layout is already based on a slimmed down subset of flexbox; Patchwork should strive to work using similar language and concepts where possible.

## Decisions

### Relative Sizing of Children in a Stack:

The `stacked` props were chosen to expose a subset of flexbox, similar to how it's done in ReactNative. Flexbox provides a solution to this in the form of the `flex-grow`, `flex-shrink`, and `flex-basis` attributes.

We can adopt a similar strategy in patchwork:

`grow`: Allows you to specify the proportional size of the element in the stack.

```jsx
<Box stacked="row">
  <TextInput grow="1" />
  <TextInput grow="2" />
  <TextInput grow="1" />
</Box>
```

In this case, the middle control will be twice as wide as the other two, and together they will fill the stack.

`shrink`: Allows you to specify the proportional "shrink factor" of the element in the stack.

```jsx
<Box stacked="row">
  <FormInput shrink="1" />
  <FormInput shrink="2" />
  <FormInput shrink="1" />
</Box>
```

In this case, the middle control will be shrink twice as much as the other two when the stack container isn't big enough to contain all the elements at their default sizes.

`basis`: Allows you to specify a fixed default size for an element, in pixels:

```jsx
<Box stacked="row">
  {/* Will be 200px wide */}
  <FormInput basis="200" />

  {/* Will consume the rest of the space */}
  <FormInput grow="1" />
</Box>
```

If the parent container is not a stack (i.e. it has no `stacked` prop) then the `basis` and `grow` properties on its children have no effect.

Both of these props can be straightforwardly implemented by patchwork using flexbox.

### Insufficient Control over Responsive Reflow

I propose to provide two approaches to alleviate this.

#### wrap

For simple cases, it is often enough to allow stacks to wrap in smaller screen sizes. We can accomplish this by allowing callers to enable flex-wrap via the `stacking` mixin:

```jsx
<Box stacked="row" wrap>
  ...
</Box>
```

### Multi-Value-Props

For more complex cases, it may be necessary to fine-tune the spacing and stacking details in different breakpoints. For this, I propose we make the `spacing` and `stacking` mixins accept breakpoint-specific values:

##### Spacing

One spacing for all breakpoints:

```jsx
<Box spacing="p2">...</Box>
```

One spacing for the small breakpoint and another one for medium and above:

```jsx
<Box spacing={{
  sm: ['ph1', 'mb1'],
  md: "p2",
}}>
```

##### Stacking - Container Props

One stacking for all breakpoints:

```jsx
<Box stacked="row">...</Box>
```

One stacking for the small breakpoint and another one for medium and above:

```jsx
<Box stacked={{
  sm: "column",
  md: "row",
}} wrap={['sm']}>
```

##### Stacking - Child Props:

Grow 1 in all breakpoints:

```jsx
<Box grow={1} />
```

Grow 1 in the small breakpoint, grow 2 in medium or greater.

```jsx
<Box grow={{ sm: 1, md: 2 }} />
```

Basis 200px in all breakpoints.

```jsx
<Box basis={200} />
```

Basis 200px in medium or greater, no basis specific in small.

```jsx
<Box grow={{ md: 200 }} />
```

### Excessive Layout-Only Boxes

To solve the problem of having to complicate your markup with a ton of extra `Box`es just for layout, I propose that we apply the `spacing`, and `stacking`, and `stackee` mixins to all block level components in patchwork, not just to `Box`.

For example,

- `Card`
- `CardSection`
- `GridItem`
- `FormInput`
- `Button`
- `Text`
  ... etc.

But not

- `Span`
- ... etc.

Form controls present a challenge in the sense that their internal padding is generally dicated by design. So we would need to expose a version of the `spacing` mixin that's margin-only.

## Complete Interface Proposal

The changes above would require the `stacking` and `spacing` interfaces to change as follows:

### Stacking

```ts
import type { Breakpoints } from 'src/breakpoints';

export type StackedProps = {|
  stacked?: StackedVariantKey | {
    [breakPoint: BreakPoints]: StackedVariantKey
  },
  justify?: JustifyVariantKey | {
    [breakPoint: BreakPoints]: JustifyVariantKey
  },
  align?: StackedAlignVariantKey{
    [breakPoint: BreakPoints]: StackedAlignVariantKey
  },
|};
```

### Spacing

```ts
type SpaceValue = SpaceKey | SpaceKey[];
export type SpaceProps = {|
  spacing?: SpaceValue | {
    [breakPoint: BreakPoints]: SpaceValue,
  },
|};
```

### Margin-Only Spacing

(For components like form controls where we don't want to allow the padding to be customized):

```ts
type MarginValue = MarginKey | MarginKey[];
export type MarginProps = {|
  spacing?: MarginValue | {
    [breakPoint: BreakPoints]: MarginValue,
  },
|};
```

### New Mixin: Stackee

```ts
export type StackeeProps = {
  grow?: number | { [breakPoint: BreakPoints]: number };
  shrink?: number | { [breakPoint: BreakPoints]: number };
  basis?: number | { [breakPoint: BreakPoints]: number };
};
```

## Consequences

The proposed changes would solve or improve the following JIRA tickets:

- https://wealthsimple.atlassian.net/browse/PAT-274 (All form inputs should expand to the width of their container)
- https://wealthsimple.atlassian.net/browse/PAT-275 (Make all block-type elements accept the spacing mixin)
- https://wealthsimple.atlassian.net/browse/PAT-281 (add support for fluid buttons at specific breakpoints)
- https://wealthsimple.atlassian.net/browse/PAT-267 (Add responsive spacing & stacking props to `Box`)
