Designing for Different Devices (Touch & Hover)

This document outlines the APIs used to adapt Discourse’s user interface for different devices.

Touch & Hover

Some devices only have touchscreens, some only have a traditional mouse pointer, and some have both. Importantly, touchscreen users cannot “hover” over elements. Therefore, interfaces should be designed to work entirely without hover states, with hover-specific enhancements added for devices that support them.

There are several ways to detect touch/hover capability via CSS and JavaScript. For consistency, we recommend using Discourse’s helpers instead of those CSS/JS APIs directly.

For CSS, you can target the .discourse-touch and .discourse-no-touch classes, which are added to the <html> element. These are determined based on the (any-pointer: coarse) media query.

For example:

html.discourse-touch {
  // SCSS rules here will apply to devices with a touch screen,
  // including mobiles/tablets and laptops/desktops with touch screens.
}

html.discourse-no-touch {
  // SCSS rules here will apply to devices with no touch screen.
}

This information is also available in Ember components via the capabilities service:

import Component from "@glimmer/component";
import { service } from "@ember/service";

class MyComponent extends Component {
  @service capabilities;

  <template>
    {{#if this.capabilities.touch}}
      This text will be displayed for devices with a touch screen
    {{/if}}

    {{#unless this.capabilities.touch}}
      This text will be displayed for devices with no touch screen
    {{/unless}}
  </template>
}

Legacy Mobile / Desktop Modes

Historically, Discourse shipped two completely different layouts and stylesheets for “mobile” and “desktop” views, based on the browser’s user-agent. Developers would target these modes by putting CSS in specific mobile/desktop directories, by using the .mobile-view/.desktop-view HTML classes, and the site.mobileView boolean in JavaScript.

These techniques are now considered deprecated and should be replaced with the viewport and capability-based strategies discussed in the next document. For backwards-compatibility, legacy desktop/mobile CSS is used when the viewport is larger/smaller than the sm threshold.

See also


This document is version controlled - suggest changes on github.

13개의 좋아요

So something like this would be deprecated?

@service site;
...
const mobileView = this.site.mobileView;

If you do it in a static context like an initializer, then yes, that’s not going to be compatible with the upcoming “viewport based mobile mode” (currently disabled by default, but will be enabled soon).

If you do the check in an autotracking context like this:

@service site;
...
<template>
  {{#if this.site.mobileView}}
    ...
  {{/if}}
</template>

Then Ember will automatically re-render things when the mobileView boolean changes (i.e. when the browser is resized). So that’s fine.

So just to be sure, putting it in a getter is deprecated, but not putting it in <template>?

Putting it in a getter is also fine, since that’ll be autotracked by Ember.

@service site;

get shouldRender(){
  return this.site.mobileView;
}

<template>
  {{#if this.shouldRender}}
    ...
  {{/if}}
</template>

^^ this is fine


A bad example would be

export default apiInitializer((api) => {
  const site = api.container.lookup("service:site");
  if(site.mobileView){
    api.renderInOutlet("some-outlet", <template>My content</template>)
  }
});

Because in this situation, mobileView is only checked when the application boots. Resizing the browser won’t re-run the initializer.

So you would refactor to something like

export default apiInitializer((api) => {
  const site = api.container.lookup("service:site");
  api.renderInOutlet("some-outlet", <template>
    {{#if site.mobileView}}My content{{/if}}
  </template>);
});

That way: changes to mobileView will take effect when the browser is resized.


An easy way to test all this: enable “viewport based mobile mode” site setting, then try resizing your browser and check that the layout updates correctly when you go between narrow/wide browser widths.

3개의 좋아요

Understood now. Thanks for the explanation!

1개의 좋아요

“Deprecation notice: Accessing capabilities.viewport.sm during the site initialization phase is not recommended. Using these values during initialization can lead to errors and inconsistencies when the browser window is resized. Please move these checks to a component, transformer, or API callback that executes during page rendering. [deprecation id: discourse.static-viewport-initialization]”

I’ve been using initialisers to switch on and off various functionality based on whether we are in a mobile view or not (e.g. changing the default Homepage, adding a community section link for example). These aren’t really meant to be dynamic and reactive - suggestions welcome on how to treat these use cases and make sure I can defeat this notice.

1개의 좋아요

The general recommendation is: don’t do that. Why should those kind of experiences differ based on the size of the screen?

A useful thought experiment is: how do you expect it to behave on folding phones or tablets, which don’t explicitly fit into the mobile/desktop buckets.

If you really do want this kind of behaviour change to be based on the user-agent of the browser (as the old mobile/desktop modes worked), then we have capabilities.isMobileDevice, which quite literally checks for the word “mobile” in the user-agent string:

1개의 좋아요

well in my case it’s that I provide the option for desktop to switch homepage to the tag intersections route - whose interface doesn’t exist on mobile … (though the route actually works - additional controls are hidden)

… but point taken, I’ll probably rethink this!

1개의 좋아요

Interesting! I wonder if that’s deliberate… I feel like it should work on any device :thinking:

2개의 좋아요

그럼 이 말은 Discourse가 pull-left를 비추천(deprecate)하게 된다는 뜻인가요? 그 클래스는 다루기 좀 불편해서 사라지는 걸 보고 싶습니다.

그랬으면 정말 좋겠어요 :wink:

해결책은 메시지 안에 있습니다.

컴포넌트를 조건부로 포함하는 대신, 항상 포함하고 컴포넌트 자체에서 조건부로 렌더링되도록 하세요.

1개의 좋아요

말씀은 이해하지만, 이 컴포넌트는 제가 관리하는 것이 아니라 코어(core)의 일부이며 특정 경로에 있습니다. 코어가 모바일과 데스크톱 모두에서 이 기능을 작동하도록 만든다면 그것이 최선의 해결책이 될 것입니다.

현재 상태에서는 이 페이지를 전역적으로 홈페이지로 설정할 수 있지만, 모바일에서는 작동하지 않으므로 무의미합니다.

현재 완전히 활용되지 않고 있고 약간 까다로운 또 다른 예시를 하나 더 드리겠습니다. 바로 카테고리 페이지입니다. 데스크톱에서는 그대로 두되, 모바일에서는 카테고리 패널을 숨기고 주제 목록만 표시하도록 ‘최신(Latest)’ 페이지로 전환하는 것이 좋을 수 있습니다. 모바일에서 카테고리 뷰만 있고 주제 목록이 없다면(홈 페이지로서) 제 생각에는 별로이기 때문입니다.

하지만 그렇게 하면 모바일에서는 더 이상 “카테고리” 페이지가 되지 않을 것입니다(라우트 이름에도 불구하고) :thinking:

이 문제는 초기화 시 장치를 식별할 수 없어 모바일 홈 페이지 강제 표시 테마 컴포넌트가 더 이상 작동하지 않는다는 사실로 인해 더욱 악화되었습니다.

따라서 탐색 라우트(interoperability)와 현재 사용 중인 장치에 대한 적합성을 위해 재검토가 필요할 것 같습니다.

3개의 좋아요