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 лайков

Значит, что-то вроде этого будет устаревшим?

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

Если вы делаете это в статическом контексте, например в инициализаторе, то да, это не будет совместимо с предстоящим «мобильным режимом на основе размера области просмотра» (в настоящее время отключён по умолчанию, но скоро будет включён).

Если же вы выполняете проверку в контексте автоотслеживания, как в этом примере:

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

то Ember автоматически перерендерит элементы при изменении булева значения mobileView (то есть при изменении размера окна браузера). Так что это не проблема.

Так что, чтобы убедиться: размещение в геттере устарело, но отсутствие в <template> — нет?

Размещение этого в геттере тоже допустимо, так как Ember будет автоматически отслеживать его изменения.

@service site;

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

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

^^ это корректно


Плохим примером будет:

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

Потому что в такой ситуации mobileView проверяется только при запуске приложения. Изменение размера окна браузера не перезапустит инициализатор.

Поэтому следует рефакторить код примерно так:

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

Таким образом: изменения mobileView будут учитываться при изменении размера окна браузера.


Простой способ проверить всё это: включите настройку сайта «Режим мобильной версии на основе области просмотра», затем измените размер окна браузера и убедитесь, что макет корректно обновляется при переключении между узкой и широкой шириной окна.

3 лайка

Теперь понятно. Спасибо за объяснение!

1 лайк

Уведомление об устаревании: Использование capabilities.viewport.sm на этапе инициализации сайта не рекомендуется. Применение этих значений во время инициализации может привести к ошибкам и несоответствиям при изменении размера окна браузера. Пожалуйста, перенесите эти проверки в компонент, трансформер или обратный вызов API, который выполняется во время рендеринга страницы. [id устаревания: discourse.static-viewport-initialization]

Я использовал инициализаторы для включения и выключения различных функций в зависимости от того, находимся ли мы в мобильном режиме (например, изменение домашней страницы по умолчанию, добавление ссылки на раздел сообщества). Эти функции не предназначены для динамического и реактивного поведения. Буду благодарен за предложения, как решить эти задачи и избежать данного уведомления.

1 лайк

Общая рекомендация такова: не делайте этого. Почему такой опыт должен различаться в зависимости от размера экрана?

Полезное упражнение для размышлений: как вы ожидаете, что это будет работать на складных телефонах или планшетах, которые явно не вписываются в категории «мобильное» или «десктоп».

Если вы действительно хотите, чтобы такое изменение поведения зависело от пользовательского агента браузера (как это работало в старых режимах для мобильных и десктопов), то у нас есть capabilities.isMobileDevice, который буквально ищет слово «mobile» в строке пользовательского агента:

1 лайк

В моём случае я предоставляю возможность для десктопной версии переключить главную страницу на маршрут пересечения тегов — интерфейс которого отсутствует на мобильных устройствах… (хотя сам маршрут работает — дополнительные элементы управления просто скрыты)

… но ваш аргумент принят, я, вероятно, пересмотрю это решение!

1 лайк

Интересно! Мне интересно, намеренно ли это… Мне кажется, это должно работать на любом устройстве :thinking:

2 лайка

Так это значит, что Discourse собирается отказаться от pull-left? Потому что с этим классом довольно неудобно работать, и я бы с радостью увидел его удаление.

Это было бы действительно здорово :wink:

Решение содержится в сообщении:

Вместо условного включения компонента всегда включайте его, а компонент должен сам условно рендериться.

1 лайк

Я вас понимаю, но этот компонент мне не принадлежит — он является частью ядра и привязан к конкретному маршруту. Если бы ядро сделало его рабочим и на мобильных устройствах, и на десктопах, это было бы лучшим решением.

В текущем виде вы можете установить эту страницу в качестве главной (глобально), но на мобильных устройствах это бессмысленно, так как она не работает.

Приведу ещё один пример, где это сейчас используется не в полной мере и вызывает определённые сложности — страница «Категории». На десктопе можно оставить как есть, а на мобильном устройстве, возможно, превратить её в страницу «Последнее», скрыв панель категорий и оставив только список тем. Поскольку наличие только вида «Категории» без списка тем (на мобильном) по моему мнению, неудачно в качестве главной страницы.

Однако тогда на мобильном устройстве это уже не будет страницей «Категории» (несмотря на название маршрута) :thinking:

Ситуация усугубляется тем, что компонент темы принудительная мобильная главная страница больше не работает, так как во время инициализации невозможно определить устройство.

Поэтому я считаю, что маршруты для обнаружения контента, возможно, потребуют переосмысления с точки зрения совместимости и пригодности для используемого устройства.

2 лайка