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개의 좋아요

이런 식의 코드가 비추천(deprecated) 대상이 되는 건가요?

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

초기화(initializer)와 같은 정적 컨텍스트에서 수행하는 경우, 곧 출시될 “뷰포트 기반 모바일 모드”(현재 기본적으로 비활성화되어 있지만 곧 활성화될 예정)와 호환되지 않습니다.

자동 추적(autotracking) 컨텍스트에서 이처럼 확인을 수행하는 경우:

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

Ember는 mobileView 불리언 값이 변경될 때(예: 브라우저가 리사이즈될 때) 자동으로 다시 렌더링합니다. 따라서 문제없습니다.

확실히 하기 위해 다시 확인하는데, getter에 넣는 것은 비추천(deprecated)이지만, <template>에 넣지 않는 것은 비추천이 아닌 건가요?

getter에 넣는 것도 괜찮습니다. 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 콜백으로 이동해 주세요. [deprecation id: discourse.static-viewport-initialization]”

저는 모바일 뷰인지 여부에 따라 다양한 기능을 켜고 끄기 위해 초기화(initialiser)를 사용해 왔습니다(예: 기본 홈 페이지 변경, 커뮤니티 섹션 링크 추가 등). 이러한 기능은 동적이고 반응형으로 동작할 의도가 아닙니다. 이러한 사용 사례를 어떻게 처리해야 하는지, 그리고 이 통지를 무시할 수 있는 방법에 대한 제안을 환영합니다.

1개의 좋아요

일반적인 권장 사항은 다음과 같습니다: 그렇게 하지 마세요. 화면 크기에 따라 그런 경험들이 왜 달라져야 할까요?

유용한 사고 실험은 다음과 같습니다: 폴더블 폰이나 태블릿처럼 모바일/데스크톱 카테고리에 명확히 해당하지 않는 기기에서는 어떻게 동작하기를 기대하시나요?

만약 브라우저의 사용자 에이전트(user-agent)를 기반으로 이러한 동작 변경이 이루어지기를 정말 원하신다면 (구식 모바일/데스크톱 모드가 작동하던 방식처럼), capabilities.isMobileDevice를 사용할 수 있습니다. 이 속성은 문자 그대로 사용자 에이전트 문자열에서 "mobile"이라는 단어를 확인합니다:

1개의 좋아요

글쎄요, 제 경우에는 데스크톱에서 홈 페이지를 태그 교차 경로로 전환할 수 있는 옵션을 제공하고 있는데, 그 인터페이스는 모바일에는 존재하지 않습니다… (다만 경로는 실제로 작동하며, 추가 컨트롤은 숨겨져 있습니다)

… 하지만 말씀하신 점은 인정합니다. 아마도 이 부분을 다시 생각해 볼 것 같습니다!

1개의 좋아요

흥미롭네요! 의도적인 건지 궁금하네요… 어떤 기기에서든 작동해야 한다고 생각하는데 :thinking:

2개의 좋아요

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

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

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

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

1개의 좋아요

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

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

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

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

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

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

3개의 좋아요