dhis2 / notes

:memo: Memos, proposals, agendas and meeting minutes
19 stars 8 forks source link

Handling different viewport sizes #59

Open Mohammer5 opened 5 years ago

Mohammer5 commented 5 years ago

I have silently added breakpoints to my table proposal in https://github.com/dhis2/ui-core/pull/304, which has triggered a discussion about if/how we should handle responsive styles.

So I think it's time we start talking about what we want to do properly (let's see if our component & design system is mature enough so we can come to a conclusion here)

Requirements

There are some requirements that we'll have to take care of:

Different layouts of components can be needed regardless of viewport dimensions

This is most likely the case for components rendered in resizable containers, here's what @cooper-joe said:

I think the 'responsive' styles are actually more useful for embedded tables than mobile, at least for the time being. Tables are often embedded in dashboard items/tracker widgets. These widgets are user re-sizable.

This can't be solved with breakpoints!

Different component-layouts based on content/page-layout

Components will most likely render differently when placed in a sidebar instead of the main content area

This can't be solved with breakpoints!

Component-layouts that change for different viewport sizes

Many component layout won't make much sense on phones and tablets (desktop view of the org unit tree or group editor for example). These changes can't be determined by the page's layout area they're placed in

This can be solved with breakpoints only!

Custom breakpoints defined by apps

Some apps will have content that's unique for those apps.

This will conflict with hard-coded breakpoints in out component libraries!

Proposal

So in order to get a grip on this problem, I'll lay out my preferred solution to this. It's won't cover all possible cases, so obviously there will be some limitations.

Final goal

What I'd like to end up with is

Custom layouts / responsive styles

I'd like to reduce custom styles to a minimum for some reasons and base most of the behavior on the page's layout:

  1. It'll reduce the amount breaking changes when working on components libraries
  2. It'll enable us to change layout behavior without having to touch apps
  3. The layout container that defines the behavior can be replaced with a custom container while keeping the area components from ui-core, so if there's really a need for something custom it still can be done
  4. Components that are placed in a page layout area should try to avoid changing as much as possible, just adjusting in a fluid fashion to width changes (but of course for some components that won't be possible)

Drawbacks

If we define behavior in our component libraries, we'll end up with some fixed (pixel based) values eventually, so we will have trouble with custom breakpoints, so this is something we'd need to avoid. With a proper/mature design system this should be do-able and even preferable

If we don't define behavior in our component libraries, we'll end up with different responsive styles in our apps and we'll have to determine component behavior with javascript because we can't use breakpoints in our component libraries.

varl commented 5 years ago

@cooper-joe mentioned a strategy that sounds good to me:

I think media queries/breakpoints work for the layout components, but the components inside of them should behave fluidly (e.g. no breakpoints, all widths fixed or percentage based). This, of course, won't work for all components, but from what I've seen so far in DHIS2 I think this makes the most sense.

Avoiding media queries in ui-core makes sense; it keeps ui-core simple and opinionated, it reduces the API surface area, and allows us to defer layout decisions to the applications, rather than carpet bomb everyone with what ui-core has decided (a concern first raised by @ismay).

Mohammer5 commented 5 years ago

Avoiding media queries in ui-core makes sense; it keeps ui-core simple and opinionated

Agree, ui-core should be free of media queries, which means that components like the table component will be split into two separate components

This, of course, won't work for all components

So how should we handle the cases where this won't work?

In my experience basing behavior on layout areas for widgets works quite well on a larger scale with the downside of not changing the behavior when needing customization.

This leads me to the next question: Currently we have ui-core and ui-widgets. How do we define where the different parts of the design system go, should it be defined by type (atoms, molecules, etc.) or on what it does (dumb -> ui-core, not-dumb -> ui-widgets)

ismay commented 5 years ago

It seems to me that in order for people to use our components, and not have to write custom ones too much (which will cause divergence in how our apps look and work) we have to find the right amount of abstraction.

I think the only place where media queries in a library component make sense to me, is if we're 100% sure of where the component will be used and what will be rendered in it. So say the headerbar. If I'm correct, we are basically in control of what gets rendered there, plus it should always be used full-width at the top. I think that using media queries there is safe.

With the table component we don't know that, so allowing users (devs) of it to choose for themselves when to render stacked vs normal is a nice compromise in my opinion. It's not the best solution performance-wise for some cases, but usability trumps performance in my opinion. That'll allow it to be used in most cases I can think of.

I think the layout components abstract things at a level that is a bit too high for me. Layouts are fairly trivial to make with grid and flexbox. I also suspect that there will be exceptions where we'll have to either complicate the layout component, or people will write their own. Maintenance-wise, handling responsive behaviour with the layout component also seems a lot more complicated to me than handling it in an app. As a dev I think I'd prefer the explicit approach with small, composable elements that are made responsive in the app, over a slightly magic layout component handling the logic for making things responsive. Given that the cost of creating a layout is not that high, but that the cost of maintaining a component like this seems fairly high to me, I'd vote against this approach.

Mohammer5 commented 5 years ago

I agree with you guys, that we shouldn't enforce a certain responsive behavior in our component libraries.


Then I'd like to talk about what options we have left:

Responsive library

We can create a library which is for the sole purpose of being used by us / in a dhis2 design-system layout context, similar to ui-widgets and leverages on ui-core.

I could imagine that components will look like this:

const useMatchMedia = condition => {
  const mq = useMemo(() => window.matchMedia(condition)}, [condition])
  const [ matches, setMatches ] = useState(mq.matches)
  const onChange = useCallback(curMq => {
    if (curMq.matches && !matches) { setMatches(true) }
    else if (!curMq.matches && matches) { setMatches(false) }
  }, [mq])

  useEffect(() => {
   mq.addListener(onChange)
   return () => mq.removeListener(onChange)
  }, [mq, onChange])

  return matches
}

const Table = props => {
  const isSmall = useMatchMedia('(max-width: 400px)')

  if (isSmall) return <TableStacked { ...props } />
  return <TableStatic { ...props } />
}

We could create common breakpoints that we'd like to use, but expose the useMatchMedia hook as well for customization in apps

Leave it to the apps

Of course apps could either implement the above or use css to hide/show the correct component (which will have the overhead of rendering all not-displayed components in the vdom)


I really want our apps to behave similar, both in terms of UX and UI. So I'd tend towards the library (which could include the layout components that I proposed as well). The advantage is that we don't mix JS and CSS in terms of responsive styles if we don't want to render all components in the vdom, so it's a clear SoC, it'd be centralized and identical in our apps.

What do you guys think? Do you have another approach that might be easier to use?

Mohammer5 commented 5 years ago

Just for reference:

Tested & works:

const useMatchMedia = condition => {
    const mq = useMemo(() => window.matchMedia(condition), [condition])
    const [ matches, setMatches ] = useState(mq.matches)
    const onChange = useCallback(curMq => {
        if (curMq.matches && !matches) { setMatches(true) }
        else if (!curMq.matches && matches) { setMatches(false) }
    }, [mq, matches, setMatches])

    useEffect(() => {
        mq.addListener(onChange)
        return () => mq.removeListener(onChange)
    }, [mq, onChange])

    return matches
}
ismay commented 5 years ago

As a small utility function that seems usable to me. I can imagine that part of this is also difficult to plan in advance. Maybe we should also just keep an eye out for emerging patterns, and centralize when and where necessary. Trying to plan it too much in advance might also be difficult.

We could create common breakpoints that we'd like to use

I would prefer not to do that. Breakpoints should differ based on the content, so to me it's not that useful to set them in advance.

ismay commented 5 years ago

Maybe we should have one or two apps where we tackle this, and then based on that discuss the patterns that we'd like to abstract, etc.

Mohammer5 commented 5 years ago

I would prefer not to do that. Breakpoints should differ based on the content, so to me it's not that useful to set them in advance.

That's true, but I think there are certain things that we already know:

Then, you can add breakpoints depending on the layout and area an element is placed inside. Which means: If you need different breakpoints because the content is different, you probably also need a different layout (which could be the same grid with different proportions)


But I think it's alright to try to see emerging patterns. The maintenance app is the newest one we currently have, so we could start creating responsive components there. And if it turns out to work nicely and could be reused across apps, extract the responsive stuff to a library

ismay commented 5 years ago

Yeah, to me the headerbar also seems like a good candidate for including breakpoints. For the layout components I'm not sure personally: https://github.com/dhis2/notes/issues/59#issuecomment-519022367.

But I think it's alright to try to see emerging patterns. The maintenance app is the newest one we currently have, so we could start creating responsive components there. And if it turns out to work nicely and could be reused across apps, extract the responsive stuff to a library

Yeah to me that seems the most practical. Otherwise it feels a bit like an academic exercise in trying to predict what we'll need.

Mohammer5 commented 5 years ago

Should be interesting to @dhis2/front-end Would like to get other opinions on this as well

HendrikThePendric commented 5 years ago

I'm not 100% sure about this, but I think we have some formally supported measure of minimum screen width. I believe >= 1024px is supported.

I remember reading about this a long time ago, but I can't really find any document that actually states this. At one point in time we we working on a Google doc, and someone added some text about the minimum width, which was removed later on because it didn't really belong in that document. So all I have is a link to a slack thread which includes a link to the Google docs comment about the deleted text. I don't think it would hold up in court....

I thought I'd mention this anyway, because it might be very relevant for this discussion.

Mohammer5 commented 5 years ago

I'm not 100% sure about this, but I think we have some formally supported measure of minimum screen width.

Yes we currently don't support smaller screens, but looking at the development of devices, there's a trend towards small, mobile devices is Asia (and I assume it's similar in Africa) and I think we should think a bit more progressively here