Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Viewport #303

Merged
merged 17 commits into from Feb 8, 2019

Conversation

Projects
None yet
3 participants
@bpierre
Copy link
Member

commented Feb 2, 2019

builds on #305

Viewport

image

Why?

Truly adaptive layouts in CSS is hard. Media queries enabled websites to adapt to the viewport width (at the global level), and window.matchMedia / window.onResize allow to get this information from JavaScript. But some essential features are still missing to get at the level of other platforms:

  • There is no native solution to conditionally apply CSS declarations, based on the dimensions of an element (“element queries”) rather than the entire viewport.
  • Only window provides a resize event, not elements themselves (e.g. resizing a sidebar inside of an app wouldn’t trigger a window.onResize event).
  • There is no unit referring to the parent, the same way we do have vw / vh. The % unit can have this role in certain cases, but it depends on the context, and don’t allow to refer to the parent width from the height property, or vice versa.

Until these problems get solved (CSS Layout API 🤞), a common solution is to combine several APIs and techniques:

  • Use global CSS Media queries when possible, using a defined set of viewport-based breakpoints.
  • Adapt React props and elements rendering, using the same global breakpoints, when possible. When local adaptations are, use custom values rather than the breakpoints.
  • Elements dimensions can be measured when the viewport gets resized if needed
  • When an element dimensions doesn’t depend on the viewport size, it is possible to keep track of them to calculate the dimensions of other elements (e.g. the horizontal padding of the sidebar).

What about <BreakPoint />?

We currently have two solutions to adapt apps to the viewport dimensions. breakpoint(), which generates CSS media queries, and <BreakPoint />, which renders its children based on matched media queries. They were created to be complementary: breakpoint() for pure CSS adaptations, and <BreakPoint /> for components.

breakpoint() could be improved (e.g. by supporting custom breakpoints and providing a better API), but it remains useful and efficient as it is. This PR doesn’t change anything about this utility, apart from adding new named breakpoints.

<BreakPoint />, on the other hand, was created in the context of a website, and is showing serious limitations in a web app context:

  • Because of the way it works, it is not possible to change the value of a prop without unmounting and mounting a component, which can cause a full rendering of an app if anything needs to be adapted near the top level of the app tree.
  • Only one media query is supported at a time.
  • Only named media queries are supported, making it hard to react to custom breakpoints, or to adapt something based on the dimensions of the viewport.

A better approach: <Viewport />

<Viewport /> combines named breakpoints with viewport dimensions, allowing any level of flexibility. Instead of relying on conditional rendering of its children, it provides a render prop that provides a set of values and utilities. A React hook (useViewport()) will be added later, providing the same features.

Note: it only relies on window.onResize: width and height are known already, so there is no need for another listener added through window.matchMedia.

How to test

  1. Run these commands in your terminal app:
git clone https://github.com/aragon/aragon-ui.git
cd aragon-ui
git checkout -b windimensions origin/windimensions
npm install
cd devbox && npm install && npm start
  1. Open http://localhost:1234/#Viewport

Usage

// The window width and height can be used directly if needed.

<Viewport>
  {({ width, height }) => (
    <div>Dimensions: {width}x{height}</div>
  )}
</Viewport>
// A set of utilities (`above()`, `below()`, `within()`) allow to handle the most
// common use cases in a concise manner. They are also taking care of excluding
// the ending value of a range, ensuring that two breakpoints ranges don’t overlap
// on their starting / ending values.

<Viewport>
  {({ above }) => (
    <App largeMode={above('medium')} />
  )}
</Viewport>


<Viewport>
  {({ below }) => (
    <TextInput wide={below('small')} />
  )}
</Viewport>

<Viewport>
  {({ above }) => (
    <TextInput wide={!above('small')} />
  )}
</Viewport>

// Custom numbers are supported too:
<Viewport>
  {({ above }) => (
    <TextInput wide={below(500)} />
  )}
</Viewport>
// It still allows for conditional rendering of components.

<Viewport>
  {({ within }) => (
    <div>
      {below('medium') && <SmallVariant />}
      {within('medium', 'large') && <MediumVariant />}
      {above('large') && <LargeVariant />}
    </div>
  )}
</Viewport>
// Breakpoint values can be used directly too.

<Viewport>
  {({ width, breakpoint }) => (
    <div css={`min-width: ${breakpoint.min}`}>
      <App largeMode={width >= breakpoint.medium} />
    </div>
  )}
</Viewport>

Remarks

Width-first API

As with <BreakPoint /> and breakpoint(), its API is focused on the width rather than the height, as it is the main axis used to adapt layouts. height is available if needed, but everything else (breakpoints and utilities) is related to the viewport width.

Rendering frequency

Getting live updates (rather than matchMedia) is powerful, but costly: the render function gets called every time the viewport dimensions are updated. It is not a huge performance issue as animations are unlikely to happen during a resize and they don’t happen often, but two solutions can mitigate it.

The first one is to throttle the updates, which Viewport does internally: updates only happen every 100ms (by default, can be changed using throttleWait).

The second one is that we should be particularly careful of skipping renders when applicable (using the usual shouldComponentUpdate() / React.PureComponent / memo()) on the immediate children of <Viewport>. This is something that should be done anyway, so I don’t see it as an issue.

If needed, we could also add a way for <Viewport> to declare that we don’t want to get live width / height updates, but only to listen to breakpoints changes.

@bpierre bpierre requested review from drcmda, AquiGorka, sohkai and delfipolito Feb 2, 2019

@bpierre bpierre changed the title Add WinDimensions Add Viewport Feb 2, 2019

bpierre added some commits Feb 2, 2019

@bpierre bpierre referenced this pull request Feb 2, 2019

Merged

Add Main #305

@bpierre bpierre referenced this pull request Feb 3, 2019

Merged

Use Viewport #601

render() {
const { windowSize } = this.state
const { children } = this.props
const { within, above, below } = this

This comment has been minimized.

Copy link
@AquiGorka

AquiGorka Feb 4, 2019

Contributor

❤️ (destructuring thyself)

// Check if the current width is between two points.
// Accepts a breakpoint string ('small', 'large') or numbers (width in pixels).
// `min` is inclusive and `max` is exclusive.
within = (...[min, max]) => {

This comment has been minimized.

Copy link
@AquiGorka

AquiGorka Feb 4, 2019

Contributor

Just wondering if between would be a better name, did you choose within for any specific reason?

This comment has been minimized.

Copy link
@bpierre

bpierre Feb 4, 2019

Author Member

between could work too yes, I don’t have any strong opinion about using one or the other. @sohkai thoughts?

This comment has been minimized.

Copy link
@sohkai

sohkai Feb 6, 2019

Member

Wondering if just (min, max) might be better (or an object options form if we think someone would want to specify max without min)?

This comment has been minimized.

Copy link
@bpierre

bpierre Feb 7, 2019

Author Member

@sohkai

Wondering if just (min, max) might be better

Ah yes! I moved from using array and I’m not sure why, but I made the args an array to destructure it, rather than using simple parameters haha.

or an object options form if we think someone would want to specify max without min

If someone really wants to use within, -1 means “no value”, e.g. within(300, -1) or within(-1, 300). But I added above() and below() for convenience. Do you think we should only have one method with named parameters (object)?

Also what do you think about within vs. between?

This comment has been minimized.

Copy link
@sohkai

sohkai Feb 8, 2019

Member

Good point, I really like the above() and below() shortcuts.

No real preference on within and between, but perhaps within is better as it's very different from below. between also suggests an open interval ((a, b)) whereas within suggests a closed interval ([a, b]) but we have a half-closed one 🤷‍♂️.

@sohkai sohkai added this to the A1 Sprint: 4.1 milestone Feb 4, 2019

@sohkai

sohkai approved these changes Feb 6, 2019

Copy link
Member

left a comment

❤️ ❤️ ❤️ Looking so forward to this!

Show resolved Hide resolved src/providers/Viewport/Viewport.js Outdated
Show resolved Hide resolved src/providers/Viewport/Viewport.js Outdated
// Check if the current width is between two points.
// Accepts a breakpoint string ('small', 'large') or numbers (width in pixels).
// `min` is inclusive and `max` is exclusive.
within = (...[min, max]) => {

This comment has been minimized.

Copy link
@sohkai

sohkai Feb 6, 2019

Member

Wondering if just (min, max) might be better (or an object options form if we think someone would want to specify max without min)?

Show resolved Hide resolved src/providers/Viewport/Viewport.js Outdated

bpierre added some commits Feb 7, 2019

@@ -106,6 +106,8 @@ class ViewportProvider extends React.Component {
}
}

// React emits a warning message if `Provider` is attached to `Consumer`, this

This comment has been minimized.

Copy link
@AquiGorka

AquiGorka Feb 8, 2019

Contributor

❤️

@AquiGorka
Copy link
Contributor

left a comment

Fiesta! 🎉 🎉

bpierre added some commits Feb 8, 2019

@bpierre bpierre merged commit 07f9555 into master Feb 8, 2019

1 of 4 checks passed

License Compliance FOSSA is analyzing this commit
Details
continuous-integration/travis-ci/pr The Travis CI build is in progress
Details
continuous-integration/travis-ci/push The Travis CI build is in progress
Details
license/cla Contributor License Agreement is signed.
Details

@bpierre bpierre deleted the windimensions branch Feb 8, 2019

@bpierre bpierre referenced this pull request Feb 26, 2019

Merged

App Center (read-only) #625

9 of 9 tasks complete
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.