Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Button] [Link] Доработка API контролов Button и Link IF-376 #2598

Open
JackUait opened this issue Oct 31, 2021 · 6 comments · May be fixed by #3267
Open

[Button] [Link] Доработка API контролов Button и Link IF-376 #2598

JackUait opened this issue Oct 31, 2021 · 6 comments · May be fixed by #3267
Labels
a11y Issues related to a11y of components major

Comments

@JackUait
Copy link
Contributor

JackUait commented Oct 31, 2021

Основано на обсуждении из #1728.
Дополнительную информацию касательно этой проблемы можно найти в #2594, в треде в слаке про Clickable, в треде в слаке про видение дизайнеров.

Терминология

Примечание: все термины из этого раздела выделены в тексте как сниппеты.

Неинтерактивная ссылка - это элемент разметки со следующими характеристиками:

  1. У такой ссылки отсутствуют атрибуты href и onClick.
  2. Курсор при наведении на такую ссылку остаётся дефолтным.
  3. При наведении на такую ссылку не должно появляться подчёркивание.
  4. Цвет такой ссылки должен наследоваться от body или html.
  5. К такой ссылке не применяются цветовые модификаторы (она всегда должна оставаться в стандартном цвете).
    Пояснение: так как неинтерактивная ссылка - это отклонение от нормального состояния, то мы не хотим вводить пользователей в заблуждение, выделяя неинтерактивный элемент другим цветом, так как пользователь может подумать, что элемент выделенный другим цветом - это интерактивный элемент.
    Хорошим примером плохого дизайна служит сайт Apple Jesus на этом сайте для ссылок используется оранжевый цвет с подчёркиванием и для выделения важной информации используется тот же оранжевый цвет, но уже без подчёркивания.

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

Постулаты

  1. Контролы Link и Button должны быть использованы в зависимости от их функциональной принадлежности, а не внешнего вида. То есть контрол Link не может быть использован как кнопка, а контрол Button не может быть использован как ссылка.
  2. Контрол Button не должен быть использован для любого типа перехода по ссылке, в том числе и программного (т.е. перехода с использованием роутера). Для этой цели должен быть использован контрол Link. Это же сказано в гайдах.
  3. Контрол Link способен выполнять дополнительное действие при клике на него.
  4. Клибабельная область контрола Button, когда этот контрол выглядит как ссылка, должна быть больше, чем кликабельная область контрола Link как описано в гайдах.
  5. Контролы Link и Button должны иметь идентичные визуальные состояния (не считая кликабельной области), чтобы у пользователя была возможность выбрать нужный ему контрол опираясь не на визуальный стиль, а на функциональную составляющую.
  6. Контрол Link должен выбрасывать ошибку, при попытке передачи атрибута onClick без атрибута href.

Аргументация

  1. Использование тега <button/> в качестве ссылки вызывает проблемы с доступностью интерфейса. Пользователи скрин-ридеров не смогут понять что нажатие на кнопку-ссылку произведёт не действие на странице, а переход по ссылке.
  2. Тег <a/>, в отличие от тега <button/> по-другому воспринимается браузерами: при нажатии правой кнопкой мыши по ссылке (или удерживании пальца на ссылке на мобильных устройствах) вызывается контекстное меню с действиями, которые можно проделать с ссылкой.
  3. При наведении на тег <a/> браузер слева снизу отображает путь на который ведёт ссылка.
  4. Использование тега <a/> в качестве кнопки может привести к проблемам с SEO. Поисковые системы могут занижать сайты в поисковой выдаче из-за использования пустых ссылок, расценив такие ссылки как способ нечестного SEO продвижения.

Ошибки гайдов

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

  1. "Ссылка связывает веб-страницы или выступает как более легкий аналог кнопки." - вторая часть этого предложения ошибочна. Кнопки служат для выполнения действия, но у ссылок уже есть основное действие - переход на ресурс указанный в href, поэтому ссылки согласно 3-му постулату могут выполнять только дополнительное действие.
  2. "Клик по ссылке открывает другую страницу или запускает действие." - согласно 3-му постулату, это определение должно звучать так: "Клик по ссылке открывает другую страницу, а также может параллельно запускать дополнительное действие".
    Клик по ссылке всегда должен открывать другую страницу (на самом деле не открывать другую страницу, а делать переход на ресурс указанный в href, из-за того, что мы рассматриваем проблему в рамках SPA/SSR это различные понятия), за исключением того случая, когда ссылка это неинтерактивный элемент.
  3. "Если ссылка запускает действие, в верстке, такая «ссылка» должна быть сделана кнопкой... ...нужно блокировать открытие такой ссылки в новом окне..." - очень запутанное правило, сначала говорится о том, что "ссылку" запускающую действие нужно делать кнопкой (что верно), затем в этом же правиле говорится о том, что нужно блокировать открытие такой ссылки в новом окне. Согласно 2-му постулату Button не может быть использован для совершения действия перехода по ссылке, следовательно и открывать в новом окне нечего.
  4. "В современных веб-интерфейсах граница между кнопками и ссылками размыта." - это не так.
    У обоих элементов есть чёткое предназначение.
    Кнопка предназначена для обработки события клика.
    Ссылка предназначена для перехода на ресурс указанный в href.
    В этом нас убеждают 1-ый, 2-ой и 3-ий постулаты, а также аргументация.
    Если кнопка выглядит как ссылка, это всё ещё кнопка, что возможно благодаря 5-му постулату.
  5. "Отличие кнопки в том, что она заметнее, и почти никогда не используется для перехода на другую страницу (то, для чего изначально задумывались гиперссылки)." - кнопка никогда не должна использоваться для перехода на другую страницу, согласно 2-му постулату.

Общие правки: правки, которые нужно внести в контролы Link и Button

  1. Проп displayAs с типом displayAs?: 'link' | 'button'. По умолчанию для контрола Link этот проп будет равен link и для контрола Button - button. Этот проп будет задавать контекст для пропа use, что позволит реализовать 5-ый постулат.
<Link displayAs="button" use="success">
	Эта ссылка будет выглядеть как контрол Button с use="success"
</Link>

При этом, есть несколько важных моментов, которые нужно учесть:

  1. Контрол Button с displayAs="link" это не совсем то же самое, что контрол Link с displayAs="link". Согласно 4-му постулату в таком случае у контрола Button кликабельная область должна быть больше, чем у контрола Link.
<Button displayAs="link"/> !== <Link displayAs="link"/> => true
  1. При этом контрол Link с displayAs="button" должен иметь такую же кликабельную область, как контрол Button с displayAs="button". Следовательно, при задании displayAs="button" внутри контрола Link мы должны менять стиль display: inline на display: inline-block.
<Button displayAs="button"/> === <Link displayAs="button"/> => true

  1. Для реализации первого пункта общих правок необходимо унифицировать значения которые можно передать в проп use.

Таблица совместимости пропов:

Link Button Цвет Общий вариант
default primary Синий primary
success success Зелёный success
danger danger Красный danger
grayed default Серый grayed
- pay Жёлтый pay
- link - Больше не нужен

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

Правки, которые нужно внести в контрол Link

  1. Согласно 6-му постулату: "Контрол Link должен выбрасывать ошибку, при попытке передачи атрибута onClick без атрибута href.".

#f03c15 Так делать не стоит.

<Link onClick={() => console.log("Так нельзя")}>
	Эта ссылка не отобразится, так как будет выброшена ошибка
</Link>;

#c5f015 А вот так делать стоит.

<Link href="htpps://www.youtube.com/" onClick={() => console.log("Так можно")}>
	С этой ссылкой всё в порядке
</Link>
  1. Если у контрола Link нет пропа href - ссылка которую этот контрол отрендерит должна быть неинтерактивной.
<Link>Такая ссылка должна быть неинтерактивной (см. терминологию)</Link>
  1. Если в контрол Link не был передан проп href - ссылка должна быть отрендерена без атрибута href.
<Link>Проп href не передан</Link> -----> <a>Проп href не передан</a>
  1. Проп size с типом size?: ButtonSize для задания размеров когда displayAs="button".
<Link size="medium" displayAs="button">Работает только когда displayAs="button"</Link>

Если передан проп size, но displayAs="link" - должна быть выброшена ошибка.

Проп size может быть передан только элементу с displayAs="button".

@JackUait JackUait added the major label Nov 1, 2021
@lossir
Copy link
Member

lossir commented Nov 8, 2021

Крутой, доскональный анализ! Долго эта проблема ждала его)

Я бы только поспорил с некоторыми предлагаемыми правками.

По сути, сейчас у нас есть 3 состояния: Ссылка, Кнопка и Кнопка-Ссылка.
Но проп displayAs добавит ещё одно - Ссылка-Кнопка:

<Link displayAs="button" /> 

Т.е. визуально это будет Кнопка, но функционально Ссылка.

Соглашусь с синхронизацией пропа use. С введением неинтерактивной ссылки и варнинга если проп onClick задан без пропа href.
Но далее предлагаю ограничится новыми пропами только для Button:

  • вынести функциональность use="link" в отдельный проп, типа asLink
  • добавить проп href, который позволит делать состояние Ссылка-Кнопка. т.е. банально менять внутри <button /> на <a />.
код состояние
<Button asLink onClick={click} /> Кнопка-Ссылка
<Button asLink href="//www" onClick={click} /> Ссылка-кнопка

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

@JackUait
Copy link
Contributor Author

JackUait commented Dec 1, 2021

Нашёл отличный сайт, на котором можно посмотреть можно ли вложить один тэг в другой.

@nulladdict
Copy link
Contributor

nulladdict commented Jul 8, 2022

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

не читал весь анализ, но удобное апи должно выглядеть так (назовем элемент Clickable):

  • as — пропса, которая говорит что должно быть корневым элементом (можно передавать теги или реакт компоненты, например Link из react-router).
<>
  <Clickable as="button" onClick={console.log} />
  <Clickable as="a" href="/href" target="_blank" onClick={console.log} />
  <Clickable as={Link} to={{ path: "/href" }} onClick={console.log} />
</>
  • variant — пропса, которая говорит как выглядит контрол (закрашенный вид, вид с обводкой, вид просто текста, и тд)
<>
  <Clickable as="button" variant="outlined" onClick={console.log} />
  <Clickable as="a" variant="filled" href="/href" target="_blank" onClick={console.log} />
  <Clickable as={Link} variant="text" to={{ path: "/href" }} onClick={console.log} />
</>

при этом не стоит стараться отгадать за пользователя какой элемент нужно рисовать, лучше всегда требовать пропсу as, потому что в реальной жизни комбинаторика вариантов слишком большая (всякие styled компоненты, компоненты-обертки, дивы, input type=submit-ы и тд)

неплохой пример есть у Material UI: https://mui.com/material-ui/react-button/

@halfee
Copy link
Contributor

halfee commented Jul 21, 2022

Тоже прокомментирую, анализ прочитал. Очень круто разобрано, это большая работа. Спасибо.
Но не со всем согласен.

1. Нужно уточнить

Непонятно что имели в ввиду в гайдах под фразой

У ссылки должна быть кликабельная область как у кнопки

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

2. Общие правки 1.

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

3. Общие правки 1.1.

Контрол Button с displayAs="link" это не совсем то же самое, что контрол Link с displayAs="link". Согласно 4-му постулату в таком случае у контрола Button кликабельная область должна быть больше, чем у контрола Link.

Это не так. Автор гайда упускает большой пласт сценариев с инлайн-кнопками. В пункте 1 я привёл пример.
Не нужно на это ориентироваться, это сломает текущие практики и поведение, которое используется в проектах уже сейчас. Кажется, стоит эти моменты обсудить с проектировщиками, напомнить / рассказать что есть сейчас. Новый гайд выглядит оторванным от реальности. Путаются кнопки / ссылки, смешиваются внешний вид и поведение. Нужно разделять внешний вид и поведение. Нужно дать обратную связь и скорректировать технические моменты.

4. Общие правки 2.

Не получится унифицировать пропы, они не идентичны. button use default это не grayed. Кнопка белая. Такое сопоставление неинтуитивно. Это натягивание совы на глобус.

Лучшим решением было бы избавляться от use, либо расширять его. Например, создать единый список представлений кликабельных контролов: button-default, button-primary, link-success и т.д. . Старые значения (временно) оставить для обратной совместимости.

Но давайте посчитаем сколько видов кнопок есть сейчас и будем помнить что фантазия проектировщиков неудержима. Проектировщики решают свои интерфейсные задачи и в любой момент может появиться необходимость в новом представлении для контрола. Где-то новое представление необходимо, где-то может показаться избыточным, нужно смотреть конкретный сценарий.
С точки зрения библиотеки контролов же нужно помнить, что всё что попадёт в use нужно будет поддерживать в будущем. Поэтому вариант унификации / расширения use именно в таком виде выглядит весьма сомнительным.

5. Правки Link 1.

Согласно 6-му постулату: "Контрол Link должен выбрасывать ошибку, при попытке передачи атрибута onClick без атрибута href.".

Категорически против самого постулата и решения бросать ошибку.

Не нужно бросать runtime ошибку, неправильно создавать разработчикам проблемы на ровном месте.
Лучше не мешать работать, а рекомендовать хорошие практики.
Достаточно дать совет в документации. Описать "гигиенический минимум" разработчика, что-нибудь вроде

"не используйте ссылку без свойства href, это противоречит спецификации и мешает доступности вашего интерфейса. Лучше применить кнопку".

В пункте приведён отличный пример с кодом, который можно перенести в документацию.

Если хотим ограничить использование - лучше это сделать на уровне тайпингов.

6. Правки Link 2

Согласен с @nulladdict, что "неинтерактивная ссылка" это оксюморон.

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

7. Правки Link 4

Не согласен с

"Если передан проп size, но displayAs="link" - должна быть выброшена ошибка."

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

Повторюсь, что направление развития контролов через дополнительный проп displayAs кажется сомнительным. Помимо size и use появляется дополнительный проп displayAs, а это дополнительный множитель для вариантов, которые придётся поддерживать: use * size * displayAs. Это запутает и усложнит работу с контролом как для рядовых разработчиков, так и для команды поддерживающей контролы.

Что в итоге?

  • в гайдах есть сомнительные утверждения, хочется уточнить что имели ввиду и возможно внести правки в гайд, чтобы и у разработчиков, и проектировщиков было одинаковое представление
  • считаю, displayAs не решит проблем, а только увеличит сложность как для потребителя, так и для команды, которая развивает контролы
  • текущий подход с use перестает удовлетворять требованиям, становится не гибким, количество вариантов растёт
  • Не нужно лишней регуляции. Контролы не должны бросать ошибки в рантайме. Есть исключения, например, контрол для ввода валюты. Точность значения при работе с деньгами очень важна для бизнес-сденария. Для кнопок и ссылок в этом нет необходимости.

@halfee
Copy link
Contributor

halfee commented Jul 21, 2022

Предложения

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

Считаю что пора отделить внешний вид контрола от функциональной принадлежности.

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

Как это можно сделать:

  • Описать все визуальные свойства кнопки и ссылки моделью
    • принцип как сейчас в теме, только без привязки к конкретному use (primary, pay)
    • так свойств будет меньше, проще найти нужный. Сейчас искать нужную переменную в теме – ад.
  • Определить дефолтный вид для ссылки и для кнопки
  • Разрешить менять дефолтный вид частичной моделью (как в теме)
  • Подготовить стандартные view для типовых кнопок

По сути предлагаю передавать в контрол маленькую локальную тему.

interface ClickableViewStyles {
  display?: 'inline' | 'inline-block';
  textDecoration?: CSSProperties['textDecoration'];
  border?: string;
  background?: string;
  /* ... */
}

interface ClickableView {
  default: ClickableViewStyles;
  hover?: ClickableViewStyles;
  focus?: ClickableViewStyles;
  active?: ClickableViewStyles;
}


const PrimaryButtonView {
  default: {
    background: "lightBlue"
  },
  hover: {
    background: "blue"
  },
  active: {
    background: "darkBlue"
  }
}

/* ------------ */
import { PrimaryButtonView } from '@skbkontur/react-ui';
import { MyProjectButtonView } from '../controls/buttons';

<Button use={PrimaryButtonView}>Подписать важный документ</>
<Link use={PrimaryButtonView}>Перейти к самому важному разделу</>

<Button use={MyProjectButtonView}>Создать что-то</Button>

@asukhar asukhar changed the title [Button] [Link] Доработка API контролов Button и Link [Button] [Link] Доработка API контролов Button и Link IF-376 Jan 30, 2023
@nulladdict
Copy link
Contributor

nulladdict commented Jun 21, 2023

Вот ещё сценарий на подумать.

Элемент, при клике на который происходит браузерное pop (если есть куда идти) или replace (если пользователь пришел по прямой ссылке)

Очевидно, что под капотом должен быть тег a, потому что у места куда этот элемент ведет есть адрес. При этот основное действие (push этого адреса) никогда не происходит, потому что ломает браузерную историю. При этом пользователь может захотеть открыть «предыдущую» страницу в новой вкладке, поэтому это точно не button.

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

@JackUait JackUait linked a pull request Sep 16, 2023 that will close this issue
@JackUait JackUait linked a pull request Mar 4, 2024 that will close this issue
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
a11y Issues related to a11y of components major
Development

Successfully merging a pull request may close this issue.

4 participants