Привет ~ это будет универсальное решение для начальной загрузки для новичков.

React.js CSS

Этот компонент имеет открытый исходный код, исходный код можно увидеть:GitHub.com/byte dance/a…

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

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

Картинка более конкретна, чем текст.Ниже приведены два типичных компонента руководства для начинающих.Вы понимаете, что такое компонент руководства по функциям с первого взгляда?

Введение в функцию

пошаговое руководство

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

способы представления

режим маски

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

всплывающий режим

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

Точное позиционирование

начальное позиционирование

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

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

автопрокрутка

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

1.gif

работа с клавиатурой

Когда появляется компонент Guide, мы хотим, чтобы внимание пользователя было полностью привлечено. Чтобы пользователи, использующие вспомогательные средства чтения, могли воспринимать внешний вид Руководства, мы перемещаем фокус страницы во всплывающее окно и делаем фокусом каждый читаемый элемент во всплывающем окне. В то же время пользователь может использовать клавиатуру (таб или таб+шифт), чтобы сфокусировать содержимое всплывающего окна по очереди, или нажать клавишу выхода, чтобы выйти из руководства.

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

2.gif

Техническая реализация

Общий процесс

Прежде чем отображать шаги компонента, мы сначала оценим, истек ли срок его действия.Существует два критерия для оценки того, истек ли срок его действия: один из них заключается в том, что загрузочный компонент находится вlocalStorageЯвляется ли уникальный ключ, хранящийся в истинным, если это правда, шаг компонента завершен. Во-вторых, компонент получаетprops.expireDate, если текущее время большеexpireDateЭто означает, что срок действия компонента истек и больше не будет отображаться.

Когда срок действия компонента не истек, он будет отображать входящиеprops.stepsСоответствующее содержание, структура шагов выглядит следующим образом:

interface Step {
    selector: string;
    title: string;
    content: React.Element | string;
    placement: 'top' | 'bottom' | 'left' | 'right'
        | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right',
    offset: Record<'top' | 'bottom' | 'left' | 'right', number>
}

const steps = Step[]

согласно сstep.selectorПолучить выделенный элемент, а затем в соответствии сstep.placementОтобразите всплывающее окно в определенном месте, связанном с выделенным элементом. При нажатии кнопки «Далее» отобразится следующий шаг по порядку.После отображения всех шагов мы поместим компонент руководства вlocalStorageУникальный ключ, хранящийся вtrue, он не будет отображаться при следующем входе.

Давайте взглянем на конкретную реализацию компонента начальной загрузки.

режим маски

Текущий компонент направляющей поддерживает два режима с маскированием или без него, а эффект отображения с маскированием показан на рисунке ниже.

Маску очень легко реализовать, это div, который заполняет экран, но как сделать так, чтобы он выделял средний элемент селектора, а также поддерживал закругленные углы? 🤔 , истина только одна, а именно - border-width

Мы получили элемент селектораoffsetTop, offsetRight, offsetBottom, offsetLeft, и установите выделенное поле соответственноborder-width, затем поставьтеborder-colorУстановите его в серый, и маска с рамкой выделения сделает это! Добавьте Div на эту выделенную коробкуpseudo-element ::afterчтобы дать ему радиус границы, отлично!

Позиционирование всплывающего окна

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

Каждый элемент HTML имеет атрибут offsetParent, доступный только для чтения, который указывает на ближайший (ссылаясь на ближайший на содержащем уровне) позиционированный элемент, содержащий элемент или ближайший элемент.table,td,th,bodyэлемент. Каждый элемент позиционируется в соответствии со своим элементом offsetParent. Например, абсолютно позиционированный элемент смещается на основе его ближайшего, не статически позиционированного родительского элемента, который является его offsetParent.

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

Шаги позиционирования

Процесс расчета позиционирования всплывающего окна выглядит примерно следующим образом:

Шаг 1. Получите элемент привязки

Через селектор в переданной в Guide информации о шаге, то есть CSS-селектор, мы можем получить элемент привязки следующим кодом:

const anchor = document.querySelector(selector);

Как получить offsetParent якоря? Этот шаг на самом деле не так прост, как вы можете себе представить. Давайте поговорим об этом шаге подробно ниже.

Шаг 2. Получите offsetParent

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

const parent = anchor.offsetParent;

Но эта строка кода не охватывает все сценарии, нам нужно рассмотреть некоторые частные случаи.

Сценарий 1: Анкерный элемент позиционируется с фиксированным

Не все элементы HTMLElements имеют свойство offsetParent. Когда анкерный элемент зафиксирован в фиксированном положении, егоoffsetParentвернутьnull. В этом случае нам нужно использоватьсодержащий блокВместо offsetParent .

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

  • Если свойство positionfixed, содержащий блок обычноdocument.documentElement.
  • Если свойство positionfixed, содержащий блок также может состоять из краев области заполнения ближайшего родительского элемента, который:

    • transformилиperspectiveЗначение неnone
    • will-changeЗначениеtransformилиperspective
    • filterЗначение неnoneилиwill-changeЗначениеfilter(Доступно только в Firefox).
    • containЗначениеpaint(например: содержать: краска;)

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

Вот код в Руководстве для поиска содержащего блока:

const getContainingBlock = node => {
  let currentNode = getDocument(node).documentElement;

  while (
    isHTMLElement(currentNode) &&
    !['html', 'body'].includes(getNodeName(currentNode))
  ) {
    const css = getComputedStyle(currentNode);

    if (
      css.transform !== 'none' ||
      css.perspective !== 'none' ||
      (css.willChange && css.willChange !== 'auto')
    ) {
      return currentNode;
    }
    currentNode = currentNode.parentNode;
  }

  return currentNode;
};
Сценарий 2: Использование Guide в iframe

В коде Guide мы часто используемwindowобъект. Например, нам нужноwindowвызов на объектgetComputedStyle()Чтобы получить стиль элемента, нам также нужноwindowобъект как элементoffsetParentдно кармана. Но мы не можем напрямую использоватьwindowОбъект, почему? На этом этапе нам нужно рассмотреть случай iframe.

Представим, что если мы используем компонент Guide в приложении со встроенным iframe, код компонента Guide находится вне iframe, а управляемая функциональная точка находится внутри iframe, то при использовании метода, предоставляемого объектом Window, мы должны хотите Вызов выполняется для объекта Window, в котором находится обведенная функциональная точка, а не для окна, в котором выполняется текущий код.

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

// Get the window object using this function rather then simply use `window` because
// there are cases where the window object we are seeking to reference is not in
// the same window scope as the code we are running. (https://stackoverflow.com/a/37638629)
const getWindow = node => {
  // if node is not the window object
  if (node.toString() !== '[object Window]') {
    // get the top-level document object of the node, or null if node is a document.
    const { ownerDocument } = node;
    // get the window object associated with the document, or null if none is available.
    return ownerDocument ? ownerDocument.defaultView || window : window;
  }

  return node;
};

В строке 8 мы видим свойство ownerDocument. Если узел является элементом DOM, он имеет свойствоownerDocument, объект документа, возвращаемый этим свойством, является основным объектом, к которому принадлежат все дочерние узлы в фактическом HTML-документе. Если это свойство используется на самом узле документа, результатом будетnull. Когда node является объектом Window, мы возвращаемwindow; когда node является объектом Document, мы возвращаемownerDocument.defaultView. так,getWindowЗатем функция охватывает все возможности для узла параметра.

Шаг 3. Установите всплывающее окно

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

 // 组件A
 const A = props => (
    <>
        <Guide
            steps={[
                {
                    ......
                    selector: '#btn1'
                },
                {
                    ......
                    selector: '#btn2'
                },
                {
                    ......
                    selector: '#btn3'
                }
            ]}
        />
        <button id="btn1">Button 1</button>
    </>
)

// 组件B
const B = props => (<button id="btn2">Button 2</button>)

// 组件C
const C = props => (<button id="btn3">Button 3</button>)

В приведенном выше коде Guide будет естественным образом отображаться в структуре DOM компонента A. Как мы смонтируем его в offsetParent компонентов B и C? На этот раз я познакомлю вас с мощными, но малоизвестными порталами React.

React Portals

Когда нам нужно отобразить компонент за пределами древовидной структуры DOM его родительского узла, мы должны сначала рассмотреть возможность использования порталов React. Порталы больше всего подходят для сцен, которым необходимо визуально визуализировать дочерние узлы за пределами их родительских узлов.В реализации компонентов Antd Modal, Popover и Tooltip мы также можем увидеть применение Portal.

Мы используемReactDOM.createPortal(child, container)Создать портал. child — это компонент, который мы хотим смонтировать, а container — компонент-контейнер, к которому смонтирован дочерний элемент.

Хотя Portal визуализируется вне структуры DOM своего родителя, он не создает полностью отдельного дерева React DOM. Как и другие дочерние узлы в дереве React, портал может получать реквизиты и контекст от родительского компонента, а также может выполнять всплывающее всплывающее окно событий.

Кроме того, в отличие от дерева React DOM, созданного ReactDOM.render, ReactDOM.createPortal применяется в функции рендеринга компонента, поэтому его не нужно размонтировать вручную.

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

const Modal = props => (
    ReactDOM.createPortal(
        <div>
            ......
        </div>,
    offsetParent);
)

После рендеринга всплывающего окна в offsetParent следующим шагом руководства является вычисление смещения всплывающего окна относительно offsetParent. Этот шаг очень сложен и требует рассмотрения некоторых особых случаев. Поясним эту часть расчета подробнее.

Шаг 4. Расчет смещения

сplacement = left, что требуется в функциональной точкеосталосьВозьмите всплывающее окно, показанное в качестве примера. Если мы напрямую подключим всплывающее окно к offsetParent элемента привязки через React Portal и зададим ему абсолютное позиционирование, его положение будет таким, как показано на следующем рисунке — верхний левый угол выровнен с верхним левым углом offsetParent .

_На картинке ниже используйтесиняя коробкаПредставленное изображение коалы — это элемент, который необходимо пометить Руководству, то есть якорный элемент;красная коробказатем идентифицирует элемент offsetParent этого элемента привязки.

И наши ожидаемые результаты позиционирования следующие:

Ссылаясь на рисунок ниже, чтобы переместить всплывающее окно из исходного положения в ожидаемое положение, нам нужно переместить всплывающее окно вниз по оси Y.offsetTop + h1/2 - h2/2 px. в,h1высота анкерного элемента,h2высота всплывающего окна.

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

Реализация и позиционирование стрелки

arrowдаmodalдочерние элементы и относительноmodalАбсолютное позиционирование, как показано на рисунке ниже, имеет двенадцать мест размещения, которые мы делим на два типа:

  1. Четыре центрирующих футляра пурпурного цвета;
  2. Оставшиеся восемь фасок в желтом цвете.

Для первого случая

Стрелка всегда располагается по центру относительно края всплывающего окна. Для верха и низа всегда правильное значение стрелки. (modal.width - arrow.diagonalWidth)/2, а верхнее или нижнее значение всегда -arrow.diagonalWidth/2.

Для левого, правого, верхнее значение стрелки равно (modal.height - arrow.diagonalWidth)/2, в то время как левый или правый-arrow.diagonalWidth/2.

Примечание:diagonalWidthширина диагонали,getReversePosition\(placement\)Чтобы получить обратную позицию переданного параметра, верхняя часть соответствует нижней, а левая соответствует правой.

Псевдокод выглядит следующим образом:

const placement = 'top' | 'bottom' | 'left' | 'right';
const diagonalWidth = 10;

const style = {
  right: ['bottom', 'top'].includes(placement)
    ? (modal.width - diagonalWidth) / 2
    : '',
  top: ['left', 'right'].includes(placement)
    ? (modal.height - diagonalWidth) / 2
    : '',
    [getReversePosition(placement)]: -diagonalWidth / 2,
};

Для второго случая

Для положения A-B из рисунка ниже видно, что смещение B всегда является фиксированным значением. Например, для всплывающего окна, значением размещения которого является верхний левый угол, значение стрелки влево всегда фиксировано, а нижнее значение равно-arrow.diagonalWidth/2.

Ниже приведен псевдокод:

const [firstPlacement, lastPlacement] = placement.split('-');
const diagonalWidth = 10;
const margin = 24;

const style =  {
    [lastPlacement]: margin,
    [getReversePosition(placement)]: -diagonalWidth / 2,
}

Реализация и позиционирование точки доступа

Поддержка компонентов начальной загрузкиhotspotфункцию, даваяdivэлемент плюс анимация меняет свойbox-shadowЭффект дышащего света реализуется по размеру, и эффект показан на рисунке ниже, на котором расположение горячей точки вычисляется относительно положения стрелки, поэтому я не буду здесь вдаваться в подробности.

Эпилог

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

Рекрутинг жесткий

Наша команда набирает сотрудников! ! !

Добро пожаловать в команду внешнего интерфейса ByteDance по монетизации бизнеса.Техническое строительство, которое мы делаем, включает в себя: обновление системы внешнего интерфейса, построение инфраструктуры командного узла, инструменты для публикации CI одним щелчком мыши, поддержку обслуживания компонентов, интернационализированный интерфейс. Общие решения, сверхмощные Опираясь на микроинтерфейсную трансформацию бизнес-систем, системы визуального построения страниц, системы бизнес-аналитики BI, интерфейсное автоматизированное тестирование и т. 100 человек точно найдут для вас сферы интересов!

Если вы хотите присоединиться к нам, нажмите на наш внутренний push-канал:

✨✨✨✨✨

Интерпретация школы: BYTE Jumping School Bootstore: Wau8zhr

Если вы хотите узнать о повседневной жизни (dòu) жизни (bī) и рабочей среде (fú) среде (lì) нашего отдела

Вы также можете нажатьчитать оригиналЯ понимаю~

✨✨✨✨✨