Упражнение на удобство использования

В Khan Academy мы работаем над обновлением баннера Подтвердите адрес электронной почты.

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

Разметка: первый проход

Вот фрагмент разметки, который управляет ссылкой. Он написан с использованием React (не говоря уже о забавном HTML-внутри-JavaScript) и возвращает один из двух элементов React на основе состояния isSent. Либо тег привязки, по которому можно щелкнуть, чтобы инициировать повторную отправку, либо простой старый диапазон для подтверждения Отправлено! сообщение.

if (this.state.isSent) {
    return <span>
        Sent!
    </span>;
} else {
    return <a
        href="javascript:void 0"
        onClick={(e) => this.handleResendClick(e)}
    >
        Resend email
    </a>;
}

Добавьте немного CSS (может быть, также написано на JavaScript?) И бац, мы закончили с этой функцией.

Но подождите, есть две вещи, которые могут затруднить использование этой функции. Давайте посмотрим на них под микроскопом.

Запах №1: Это действительно ссылка?

‹a href="javascript:void 0'› - наш первый красный флаг. Тег привязки действительно должен ссылаться на другой фрагмент контента, и предотвращение этой навигации с помощью «javascript: void 0» (или href = «#») противоречит интуиции, если мы сделаем шаг назад и посмотрим, что мы создаем. На самом деле нам просто нужно что-то, на что мы можем нажать, и у нас есть идеальный элемент для этого - ‹button›.

Так что давайте изменим наш ‹a› на ‹button›. Мы можем удалить странный href, и теперь у нас будет хороший семантический элемент, который пользователи программ чтения с экрана понимают как «кнопку», а не как «ссылку», которая может привести их в другое место. Мы также бесплатно получаем события щелчка с пробелом.

if (this.state.isSent) {
    return <span>
        Sent!
    </span>;
} else {
    return <button onClick={(e) => this.handleResendClick(e)}>
        Resend email
    </button>;
}

Запах № 2: Управление фокусом

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

Рассмотрим следующую гифку, на которой я бегаю по сайту:

После нажатия кнопки «Отправить еще раз» вы увидите, как курсор фокуса исчезает, и мне приходится переходить на табуляцию с начала страницы.

На наших посетителей, использующих мышь, это не повлияет, но любой, кто использует клавиатуру по любому количеству причин, останется в неведении, поскольку их курсор фокуса вырывается прямо из-под них. В идеале следует сосредоточить внимание на итоговом сообщении «Отправлено!» текст, так что последующие табуляции или shift + tab перемещают нас вперед и назад с того места, где мы ожидаем оказаться.

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

Так почему это происходит? Если вы помните нашу предыдущую разметку, мы переходим от ‹button› к ‹span›, когда нажимаем «Отправить электронное письмо повторно». ‹Button› отключен, и фокус перемещается вместе с ним.

if (this.state.isSent) {
    return <span>
        Sent!
    </span>;
} else {
    return <button onClick={(e) => this.handleResendClick(e)}>
        Resend email
    </button>;
}

Мы можем исправить это несколькими способами. Один из распространенных методов работы с динамическим контентом - вручную сфокусировать элемент, который был заменен местами. Однако для этого требуется немного кода. Сначала мы изменим наш метод render так, чтобы (а) позволить ‹span› фокусироваться с помощью атрибута tabindex и (б) сохранить ссылку на диапазон, чтобы мы могли сфокусировать его. позже.

render() {
    if (this.state.isSent) {
        return <span
            tabindex="0"
            ref={(node) => this.spanNode = node}
        >
            Sent!
        </span>;
    } else {
        return <button onClick={(e) => this.handleResendClick(e)}>
            Resend email
        </button>;
    }
}

Затем мы добавим обратный вызов к нашему setState в handleResendClick.

handleResendClick(e) {
    this.setState({
        isSent: true,
    }, () => {
        // After setting state, manually
        // focus the new span
        this.spanNode.focus();
    });
}

Замечательно - теперь курсор фокуса остается на месте, и мы можем перемещаться вперед и назад без необходимости начинать с самого начала. Но мы можем сделать еще один шаг лучше.

Что, если бы мы вообще не убирали кнопку «Отправить письмо повторно»? Что, если вместо этого мы оставим кнопку и просто изменим ее внешний вид? Что бы "Отправлено!" как кнопка?

if (this.state.isSent) {
    // Disable our button since clicking it no longer
    // has an effect, but give it a tabindex of -1 so
    // that it stays focused.
    return <button disabled tabindex="-1">
        Sent!
    </button>;
} else {
    return <button onClick={(e) => this.handleResendClick(e)}>
        Resend email
    </button>;
}

И снова курсор фокуса остается на месте! Наша исходная кнопка больше не удаляется из модели DOM, и мы можем сохранять фокус без необходимости в дополнительном коде для ручной фокусировки нового «Отправлено!» текст.

Выводы

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

  • Курсор фокуса пользователя нельзя отрывать от них.
  • Убедитесь, что вы используете соответствующий тег для работы.

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

Спасибо за чтение :) Не забудьте подписаться на меня в твиттере, где я много разглагольствую об этом и время от времени говорю о моем новом инструменте Shade - лучшем инструменте контрастирования в галактике 🚀

Дополнительное чтение