предисловие
Компонент раскрывающегося списка Select может быть одним из наиболее часто используемых компонентов пользовательского интерфейса. Из-за этого собственный HTML также имеет этот тег. Однако из-за стремления к UI и унифицированным спецификациям мы часто не используем нативный тег Select, который не выглядит ни красиво, ни единообразно, а реализуем его сами. Нетрудно написать компонент Select, который «может использоваться в большинстве сценариев». Только когда я столкнулся с некоторыми особыми сценариями, я понял, что выполнить работу на уровне библиотеки компонентов непросто. В этой статье объясняются проблемы, возникающие в реальной производственной среде, иrc-selectСпособ решения проблемы в исходниках.
неправильный пример
В недавней разработке проекта необходимо реализовать компонент Select. В соответствии с принципом «изобретение велосипеда делает меня счастливым», открытие VSCode — это приятная операция. Пока пользователь, которому было не по себе, не прислал мне гифку:
«ОШИБОЧНАЯ» версия компонента Select относительно проста в реализации, относительно позиционированный Selection + абсолютно позиционированный DropdownMenu. Для приведенной выше реализации я сделал грубый вывод, что проблемы будут в следующих трех сценариях:
- родительский контейнер
overflow: auto
, ниже расположен компонент Select. - родительский контейнер
overflow: hidden
, ниже расположен компонент Select. - Когда уровень родительского контейнера ниже, элемент высокого уровня совпадает с позицией DropdownMenu.
Для приведенных выше сценариев мы сделали простую демонстрацию соответственно.
онлайн предварительный просмотрУчитывая, что ни один из вышеперечисленных сценариев не является нишевым, компонент Select этой «ошибочной версии» явно не соответствует требованиям.
первый инстинкт
На самом деле, если вы относительно опытный партнер, столкнувшийся с такой проблемой, вы должны быть приучены к понятию «рендеринг в теле». (Что такое «рендеринг в теле»? В проекте React для компонентов, которые необходимо отображать на самом высоком уровне, можно избежать влияния других компонентов, сохранив при этом реализацию компонентного написания. Наиболее типичным компонентом является Модальный. Для подробности, пожалуйста, обратитесь к я писал ранееСвязанное резюме) Однако проблема компонента Select будет намного сложнее, чем общий «рендер в теле», давайте реализуем его таким образом, и обобщим проблемы, которые необходимо решить, в следующие два пункта, и исследуем с этой целью.Ant Designисходный код связанных компонентов.
- Как избежать влияния других элементов на DropdownMenu? А влияние на другие элементы DropdownMenu? (рендеринг в теле)
- Selection и DropdownMenu разделены на разных уровнях DOM, как вычисляется относительное положение? Когда страница прокручивается, можно ли гарантировать, что их положение останется неизменным?
(Для удобства написания далее будем единообразно вызывать триггерную область компонента Select Selection, а выпадающее меню будет DropdownMenu)
Render in body
«рендеринг в теле» — лучший способ решения ряда проблем в проектах React, хотя я много раз убеждался в его преимуществах. Тем не менее, с точки зрения конкретной реализации, раздельная гранулярность Ant Design все же заслуживает изучения.Portal.js— это абстракция в библиотеке Ant Design, которая специально реализует эту функциональность. В компоненте Select DropdownMenu будет отображаться через Portal.js для решения вышеуказанной проблемы 1. Конкретную логику можно упростить до следующих моментов:
- componentDidMount: создайте div под корневым узлом и назначьте его
this._container
. - render:
return ReactDOM.createPortal(this.props.children, this._container)
(вthis.props.children
содержит выпадающее меню) - componentWillUnmount: удалить
this._container
Вот некоторые ключевые коды
// Portal.js
export default class Portal extends React.Component {
componentDidMount() {
this.createContainer();
}
componentWillUnmount() {
this.removeContainer();
}
createContainer() {
this._container = this.props.getContainer();
this.forceUpdate();
}
render() {
if (this._container) {
return ReactDOM.createPortal(this.props.children, this._container);
}
return null;
}
}
// 上述组件的this.props.getContainer
getContainer = () => {
const { props } = this;
const popupContainer = document.createElement('div');
popupContainer.style.position = 'absolute';
popupContainer.style.top = '0';
popupContainer.style.left = '0';
popupContainer.style.width = '100%';
// mountNode: 划重点,后文详细叙述
const mountNode = props.getPopupContainer ?
props.getPopupContainer(findDOMNode(this)) : props.getDocument().body;
mountNode.appendChild(popupContainer);
return popupContainer;
}
Расчет положения и синхронизация прокрутки
Поскольку DropdownMenu расположен в позиции узла body, он включает в себя вычисление позиции Selection и DropdownMenu. Исходный код для рендеринга DropdownMenu можно упростить до следующей структуры:
<Protal>
<Animate>
<Align>
<DropdownMenu/>
</Align>
</Animate>
</Protal>
вProtal
заключается в том, чтобы отдать Детей под тело,Animate
заключается в управлении анимацией показа/свертывания, аAlign
Этот пакет используется для расчета местоположения.в большинстве случаев, позиция Selection относительно страницы статична и естественным образом прокручивается по мере прокрутки страницы. DropdownMenu существует под телом в виде абсолютного позиционирования, и оно естественно прокручивается вместе с прокруткой страницы, поэтому пока вычисляется положение Selection относительно страницы, назначение можно немного подкорректировать для DropdownMenu в соответствии с потребностями пользователя.
Идея расчета: расстояние элемента относительно видимой областиelement.getBoundingClientRect.top/left
+ расстояние прокрутки страницыdocumentElement.scrollTop/Left
Вот и все. (Конкретные детали расчета очень изобретательны и сложны, и следующее будет унифицировано)
Код ключа следующий:
// dom-align src/utils.js
function getOffset(el) {
// 获取相对可视区的距离
const pos = getClientPosition(el);
const doc = el.ownerDocument;
const w = doc.defaultView || doc.parentWindow;
// 加等页面滚动距离
pos.left += getScrollLeft(w);
pos.top += getScrollTop(w);
return pos;
}
дальнейшее обсуждение
В приведенном выше решении задачи расчета позиции и синхронной прокрутки для простоты понимания мы по умолчанию используем точку зрения:
В большинстве случаев позиция Selection относительно страницы статична и естественным образом прокручивается по мере прокрутки страницы.
В реальных сценариях Selection, скорее всего, будет находиться в независимой области прокрутки, а не прокручиваться естественным образом вместе с прокруткой страницы.
На картинке выше Selection находится в отдельной области прокрутки, а DropdownMenu — под телом. Отсюда и ситуация на картинке:- При прокрутке на уровне страницы позиции Selection и DropdownMenu гарантированно синхронизируются.
- Когда независимая область, где прокручивается выделение, позиция будет перепутана.
Как это решить? существуетAnt Design Выберите компонентыдокументации, есть специальный реквизит:
В приведенном выше коде для рендеринга DropdownMenu есть примечание, на которое стоит обратить внимание:
getContainer = () => {
// ...
const mountNode = props.getPopupContainer ?
props.getPopupContainer(findDOMNode(this)) : props.getDocument().body;
mountNode.appendChild(popupContainer);
return popupContainer;
}
Если пользователь устанавливает реквизитgetPopupContainer
, здесьmountNode
Будет родителем прокрутки, в котором расположен выбор, т. е. DropdownMenu будет отображаться под родительским элементом прокрутки выбора вместо «рендеринга в теле».
Ставьте с правильными настройкамиgetPopupContainer
Скриншоты Chrome Element для ознакомления:
При вычислении позиции DropdownMenu стратегия алгоритма dom-align очень умна, позволяя избежать проблемы различения того, является ли родительский элемент прокрутки телом, но это слишком сложно.
(Следующие процедурыtop
значение в качестве примера,left
стоимость одинаковая)
- пройти через
element.getBoundingClientRect
Вычислить абсолютное положение относительной видимой области выделенияtop1
. - Вычислить абсолютное положение DropdownMenu относительно видимой области с помощью свойств, установленных пользователем (т. е. ориентация, интервал и т. д.)
top2
. - Установите верхнее значение DropdownMenu на -9999 и передайте
element.getBoundingClientRect
Получить текущее верхнее значение DropdownMenutop3
.
- Если DropdownMenu находится под телом,
top3 = 0 - 9999
. - Если DropdownMenu не расположен под телом,
top3 = 滚动父级至body的距离 - 9999
.
-
top4
=top2 - top3
=top2 - (滚动父级至body的距离 - 9999)
=top2 - 滚动父级至body的距离 + 9999
-
top5
=-9999 + top4
=-9999 + top2 - 滚动父级至body的距离 + 9999
=top2 - 滚动父级至body的距离
наконец-то,top5
Будет значением истинного стиля, установленным в DropDownMenu. Ввиду тонкости исходного кода специально не отображается. Адрес источника,GitHub.com/one и/do…
Суммировать
Есть много преимуществ от чтения исходного кода.Ввиду ограниченного места я перечислю ключевые моменты, чтобы поделиться с вами и обсудить вместе. Уровень ограничен, если вы не правы, можете указать.
Связанные библиотеки с открытым исходным кодом: