Искусство компонентов. Пишем карточку контакта Facebook Messenger

Автор оригинала: Ahmad Shadeed
  • Перевод
Вполне возможно оценить компонент и сказать, что он легко пишется на HTML и CSS. Соглашусь, это легко, когда вы работаете, только чтобы практиковаться, но в реальном проекте всё по-другому. Идеальный адаптивный компонент, который вы только что создали, быстро перестаёт работать, когда сталкивается с реальным контентом настоящего проекта. Почему? Потому, что, пока вы рассуждаете о разработке компонента, вы можете упустить крайние случаи. Сегодня, специально к старту нового потока нашего курса по веб-разработке, поделюсь переводом статьи, в которой показан простой на первый взгляд компонент, за которым стоит огромная работа. Ради реалистичности это будет пример прямо из Facebook Messenger.




Начнем


Я беру очень простой компонент Facebook Messenger, посмотрите на скриншот ниже:


В этом сайдбаре списком карточек перечисляются люди, которым я писал на Facebook. Здесь меня интересует только карточка. Как вы напишете её на HTML/CSS? Да очень легко, правда? Есть соблазн сказать, что это всего лишь картинка и слой рядом с ней. Вот о чём можно подумать:



Случай выше имеет место, если вы только учитесь или соревнуетесь на UI-челлендже, но когда хочется написать надёжную карточку, которая растягивается, то пример выше быстро перестаёт работать. Посмотрим на простой вариант:

<div class="card">
  <img class="card__image" src="assets/shadeed.jpg" alt="" />
  <div>
	<h3>Ahmad Shadeed</h3>
	<p>You: Thanks, sounds good! . 8hr</p>
	<img class="card__seen" src="assets/shadeed.jpg" alt="" />
  </div>
</div>

.card {
  position: relative;
  display: flex; /* [1] */
  align-items: center; /* [2] */
  background-color: #fff;
  padding: 8px;
  border-radius: 7px;
  box-shadow: 0 3px 15px 0 rgba(0, 0, 0, 0.05);
}

.card h3 {
  font-size: 15px;
}

.card p {
  font-size: 13px;
  color: #65676b;
}

.card__image {
  width: 56px;
  height: 56px;
  border-radius: 50%;
  margin-right: 12px;
}

.card__seen {
  position: absolute; /* [3] */
  right: 16px;
  top: 50%;
  transform: translateY(-50%);
  width: 16px;
  height: 16px;
  border-radius: 50%;
}

Я выделил несколько строчек, их я хочу объяснить:

  1. Использовался flexbox, потому что у нас горизонтальный дизайн, а flexbox хорошо подходит для него.
  2. Дочерние элементы нужно центрировать вертикально.
  3. Значок позиционирован абсолютно, также он центрируется вертикально.

Ломаем компонент


Здесь нет ничего плохого, но компонент не масштабируется, так что покажу другой вариант:



Синий значок справа означает, что пришло новое сообщение, которое я ещё не открывал. Зелёный цвет на аватаре показывает, что пользователь сейчас в сети.

Обратите внимание: у нас есть два новых значка. Как лучше добавить их на карточку? Если вы обратитесь к CSS, который я написал для самого первого компонента, то увидите, что там есть класс .card_seen для маленьких аваторов пользователей справа. В этом варианте .card_seen должен быть заменён синим значком. С уже написанными HTML и CSS, не изменив HTML, написать такое невозможно.

Для ясности уточню, что вариант, который я показываю, — только поверхность возможностей. У компонента много вариаций и случаев использования.

Все вариации


Ниже я показываю все вариации компонента. Очень старался описать их все (да, я нарисовал всё это вручную).



Но и этого мало: мы должны учитывать стили тёмной темы.



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

Интервалы


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



В разработке и реализации UI на HTML и CSS одна из ключевых вещей, которой нужно уделить внимание, — это интервалы. Если интервалы недооценить, позже, возможно, придётся изменить сам UI.

Области компонента


Чтобы написать приличный компонент, нужно сначала тщательно продумать разметку. У нас есть две области компонента с несколькими вариациями: аватаром и областью контента.

Аватар




Чтобы работать над HTML аватара, сначала нужно разобраться с его состояниями. Вот возможные варианты:

  • Один аватар.
  • Один аватар со значком онлайн-статуса.
  • Несколько аватаров для группового чата.
  • Несколько аватаров со значком онлайн-статуса.

Учитывая HTML ниже, мы хотим удостовериться, что .card__avatar работает со всеми вариантами аватаров выше.

<div class="card">
  <div class="card__avatar"></div>
  <div class="card__content">
	<!-- Name, message, badge.. -->
  </div>
</div>

Один аватар


Давайте увеличим масштаб HTML и сосредоточимся на первом варианте, то есть на одном аватаре. У аватара должна быть граница (или внутренняя тень), чтобы он выглядел как круг, даже если он полностью белый.



В CSS невозможно применить внутреннюю box-shadow к элементу img. У нас есть два варианта:

  • Дополнительный div с прозрачным border.
  • Можно написать svg.

Цель внутренней границы — показать контур вокруг аватара в случаях, когда у нас:

  • Полностью белый аватар светлой темы;
  • Полностью чёрный аватар тёмной темы.



Без внутренней границы полностью белый аватар смешается с родительским фоном. Это случится и с тёмной темой. Вот что отрисовывается с внутренней границей и без неё.



div для внутренней границы


В этом решении дополнительный элемент (здесь это div) абсолютно расположен над изображением с непрозрачностью 0.1.

<div class="card__avatar">
  <img src="assets/shadeed.jpg" alt="" />
  <div class="border"></div>
</div>

.card__avatar {
  position: relative;
}

.card__avatar img {
  width: 56px;
  height: 56px;
  border-radius: 50%;
}

.border {
  position: absolute;
  width: 56px;
  height: 56px;
  border: 2px solid #000;
  border-radius: 50%;
  opacity: 0.1;
}

Это решение работает, но у него есть ограничения, о которых я скоро расскажу.

Работаем с svg


В этом решении воспользуемся элементом svg. Вот идея: использовать круглую маску для аватара, а для внутренней границы — элемент circle. SVG отлично подходит для этого.

<svg role="none" style="height: 56px; width: 56px">
  <mask id="circle">
	<circle cx="28" cy="28" fill="white" r="28"></circle>
  </mask>
  <g mask="url(#circle)">
	<image
	  x="0"
	  y="0"
	  height="100%"
	  preserveAspectRatio="xMidYMid slice"
	  width="100%"
	  xlink:href="/assets/shadeed.jpg"
	  style="height: 56px; width: 56px"
	></image>
	<circle class="border" cx="28" cy="28" r="28"></circle>
  </g>
</svg>

.border {
  stroke-width: 3;
  stroke: rgba(0, 0, 0, 0.1);
  fill: none;
}

Оба решения хороши, когда строится один аватар только как пример. Всё станет интереснее, когда мы добавим элемент значка онлайн-статуса.

Единственный аватар со значком онлайн-статуса


В режиме светлой темы зелёный значок обведён белой границей. Но в тёмном режиме значок должен быть вырезан из самого аватара. Другими словами, нужно применить маску.



Как это сделать? Оказывается, если воспользоваться SVG-решением для того самого единственного аватара, проблема легко решается при помощи маски SVG.


<svg role="none" style="height: 56px; width: 56px">
  <mask id="circle">
	<!-- [1] -->
	<circle cx="28" cy="28" fill="white" r="28"></circle>
	<!-- [2] -->
	<circle cx="48" cy="48" fill="black" r="7"></circle>
  </mask>
  <!-- [3] -->
  <g mask="url(#circle)">
	<image
	  x="0"
	  y="0"
	  height="100%"
	  preserveAspectRatio="xMidYMid slice"
	  width="100%"
	  xlink:href="/assets/shadeed.jpg"
	  style="height: 56px; width: 56px"
	></image>
	<circle class="border" cx="28" cy="28" r="28"></circle>
  </g>
</svg>

Позвольте объяснить этот код:

  1. Круг маскирует аватар.
  2. В правом нижнем углу аватара вырезается маленький кружок.
  3. Группа, которая содержит circle и image для прозрачной внутренней границы.

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



Так выглядит HTML аватара со значком онлайн-статуса.

<div class="card__avatar">
  <svg role="none" style="height: 56px; width: 56px">
	<mask id="circle">
	  <circle cx="28" cy="28" fill="white" r="28"></circle>
	  <circle cx="48" cy="48" fill="black" r="7"></circle>
	</mask>
	<g mask="url(#circle)">
	  <image
		x="0"
		y="0"
		height="100%"
		preserveAspectRatio="xMidYMid slice"
		width="100%"
		xlink:href="/assets/shadeed.jpg"
		style="height: 56px; width: 56px"
	  ></image>
	  <circle class="border" cx="28" cy="28" r="28"></circle>
	</g>
  </svg>
  <div class="badge"></div>
</div>

.card__avatar {
  position: relative;
  display: flex;
  margin-right: 12px;
}

.badge {
  position: absolute;
  right: 3px;
  bottom: 3px;
  width: 10px;
  height: 10px;
  background: #5ad539;
  border-radius: 50%;
}

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

:root {
  --primary-text: #050505;
  --secondary-text: #65676b;
  --bg-color: #fff;
}

html.is-dark {
  --primary-text: #e4e6eb;
  --secondary-text: #b0b3b8;
  --bg-color: #242526;
}

.card {
  background-color: var(--bg-color);
}

.card__title {
  color: var(--primary-text);
}

.card__subtitle {
  color: var(--secondary-text);
}



Несколько аватаров в групповом чате


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

.card__avatar {
  width: 56px;
  height: 56px;
}


Этот вариант требует изменить разметку вот так:

<div class="card__avatar card__avatar--multiple">
  <svg
	class="avatar avatar-1"
	role="none"
	style="height: 36px; width: 36px"
  ></svg>
  <svg
	class="avatar avatar-2"
	role="none"
	style="height: 36px; width: 36px"
  ></svg>
  <div class="badge"></div>
</div>

.card__avatar--multiple {
  position: relative;
  width: 56px;
  height: 56px;
}

.card__avatar--multiple .avatar {
  position: absolute;
}

.card__avatar--multiple .avatar-1 {
  right: 0;
  top: 0;
}

.card__avatar--multiple .avatar-2 {
  left: 0;
  bottom: 0;
}

.card__avatar--multiple .badge {
  right: 6px;
  bottom: 6px;
}



Контент


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



Я могу представить разделённую на две части разметку, одна — для текста (имени, сообщения или действия), а вторая — для индикатора справа (новое сообщение, увиденное, приглушённое, отправленное).



Первая часть


Давайте внимательно посмотрим на разметку области контента.

<div class="card__content">
  <div class="card__content__start">
	<h3>Ahmad Shadeed</h3>
	<div class="row">
	  <p>You: Thanks, sounds good. What about doing a webinar, too?</p>
	  <span class="separator">.</span>
	  <time>8hr</time>
	</div>
  </div>
  <div class="card__content__end">
	<!-- The indicator (new message, seen, muted, sent) -->
  </div>
</div>

.card__content {
  display: flex;
  flex: 1;
}

.card__content__start {
  display: flex;
  flex: 1;
}

.card__content__start .row {
  display: flex;
  align-items: center;
}

.card__content__end {
  display: flex;
  justify-content: center;
  align-items: center;
  margin-left: 12px;
}

.separator {
  margin-left: 4px;
  margin-right: 4px;
}

С кодом выше область содержимого выглядит, как показано ниже (это скриншот из Firefox).



Сообщения или имя могут оказаться очень длинными. Важно учесть это сразу. Сначала посмотрим на подход «поток, как вам нравится».



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

  • Установите min-width: 0 для дочерних элементов flex. Зачем? Я расскажу позже.
  • Обрежьте текст через свойства overflow, white-space, и text-overflow. Я уже писал подробнее об обработке короткого и длинного контентов.

Я добавил к имени и абзацу код ниже:

.card__content__start h3,
.card__content__start p {
  overflow-x: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

Но этот код не решает нашу проблему автоматически, когда мы используем flexbox. Обратите внимание на то, что делает приведённый выше CSS:



И вот причина: flex-элементы не сжимаются сильнее минимального размера контента. Чтобы решить эту проблему, нужно установить min-width: 0 в .card__content и card__content__start.



Вторая часть


У сообщения каждого типа есть некий индикатор, мы должны учитывать индикаторы всех типов.



В этой части сосредоточимся на .card__content__end и на содержании внутри него.

<div class="card__content">
  <div class="card__content__start">
	<!-- The name and message -->
  </div>
  <div class="card__content__end">
	<!-- The indicator (new message, seen, muted, sent) -->
  </div>
</div>

У элемента .card__content__end не должно быть таких стилей, как цвет или размер шрифта, этот элемент будет служить только родительским элементом определённого компонента.

Новое сообщение


Я посмотрел, как Facebook работает с индикатором нового сообщения; оказалось, что это кнопка с надписью «Mark as read».

<div role="button" aria-label="Mark as read" tabindex="0"></div>



Не знаю, почему команда Facebook выбрала div, а не button. С встроенной кнопкой не нужны атрибуты role, aria-label и tabindex. Все они встроены в кнопку.

Единственный аватар около поста


Такой аватар ничем не отличается от аватара пользователя. В нем применяется элемент svg с атрибутом aria-label, который показывает имя пользователя.


<svg aria-label="Ahmad Shadeed" role="img">
  <!-- Mask and image -->
</svg>


Несколько аватаров около поста


Если честно, это мой любимый вариант. Мне очень нравится, как это сделала команда Facebook.


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

Граница сделана при помощи маски SVG. Да, вы не ослышались!


Маска работает так:



Невероятно. Конкретно здесь мне нравится пользоваться SVG.

Контент справа налево


Когда макет LTR (слева направо), а текст сообщения написан на арабском языке, направление текста тоже должно быть RTL (справа налево).



Элемент .card__content__start — это flex-контейнер, поэтому дочерние элементы будут автоматически переворачиваться в зависимости от значения свойства direction у компонента или корневого элемента. Такое поведение можно добавить динамически, в зависимости от языка текста.

<div class="card__content">
  <div class="card__content__start" style="direction: rtl"></div>
  <div class="card__content__end"></div>
</div>

Переворачиваем компонент


Если пользователь выбрал направление справа налево (например, арабский язык) для всего пользовательского интерфейса, то компонент нужно перевернуть.



Элементы размещаются с помощью flexbox, поэтому нужно только перевернуть поля.

/* LTR */
.card__content__end {
  margin-left: 12px;
}

/* LTR */
.card__content__end {
  margin-right: 12px;
}


Доступность


Работа с клавиатуры


Продукт, который работает с миллиардами пользователей, должен быть доступен для всех. Что касается компонента из этой статьи, я протестировал его в Chrome и Firefox и заметил такие проблемы:

  • Стили фокуса отлично работают в Chrome, но в Firefox нет визуальной подсказки.
  • Фокуса на меню действий, которое появляется при наведении курсора, можно добиться в Firefox, и я не могу получить к нему доступ с клавиатуры в Chrome.



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



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

Список карточек


В списке карточек прописаны некоторые роли ARIA. Этот список содержит строки и выглядит как сетка. В каждой строке может быть одна или несколько ячеек.

<div role="grid">
  <div role="row">
	<div role="gridcell">
	  <a href="#">
		<!-- The component lives here -->
	  </a>
	</div>
  </div>
  <div role="row">
	<div role="gridcell">
	  <a href="#">
		<!-- The component lives here -->
	  </a>
	</div>
  </div>
</div>

Несколько аватаров


Для группового чата есть несколько аватарок индикатора просмотров. Здесь роли ARIA располагают ячейки в ряд.
<div role="grid">
  <div role="row">
	<!-- 1st avatar -->
	<div role="cell"></div>
	<!-- 2nd avatar -->
	<div role="cell"></div>
  </div>
</div>

Посмотрите на демо с сайта Codepen. Всех вариантов здесь нет, я просто проверял их.

Заключение


В этой статье я хотел бы подчеркнуть, что простейший компонент требует огромной работы. Между прочим, все объяснения выше касались только HTML и CSS. А как насчёт JavaScript? Это уже другая история.

Я наслаждался работой, пока писал эту статью, и обязательно поработаю над чем-то подобным в будущем. И еще рад сообщить вам, что я написал электронную книгу об отладке CSS. Если вам интересно, кликните по ссылке debuggingcss.com и посмотрите книгу бесплатно. Вам понравился мой контент? Тогда вы можете заплатить за мой кофе. Большое спасибо!




SkillFactory
Школа Computer Science. Скидка 10% по коду HABR

Комментарии 6

    +4
    Астрологи объявили неделю бумажного самолётика на синем фоне. Количество пользователей покинувших сервисы FB увеличивается вдвое.
      0
      только собирался читать в оригинале, а тут уже перевод подъехал :) спасибо большущее!
        0
        Пожалуйста! :-)
        0

        Столько работы ради одной карточки в мессенджере о_О) спасибо, one-two

          0
          Так, пора разобраться с масками в SVG
            +1
            Почему, я так себя ненавижу, каждый раз, когда читаю подобные статьи?
            Автор молодец, хорошая работа и перевод хороший.

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

            Самое читаемое