Вы могли бы подумать, что это легко. Но это не так.

Глава 0: Окружающая среда

Весь этот пост основан на приложении React, поэтому, хотя это и не обязательно, было бы полезно, если бы у вас были хотя бы базовые знания о React и React Hooks.

Глава 1: Первоначальная проблема

В проекте, над которым мы недавно работали, мы столкнулись с очень специфической проблемой: клиент хотел большую таблицу.

Что я имею в виду под большим? Я видел таблицы с 15+ столбцами, которые намного шире экрана. К счастью, это был не один из тех столов, но он все равно был шире доступного места.

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

Чтобы добиться этого, мы обернули таблицу в <div>, width которой был привязан к определенному значению (примерно 80% ширины экрана). Затем мы добавили несколько кнопок и функцию scrolling, которая изменяет положение прокрутки обёртки. Функция прокрутки выглядела примерно так:

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

Это было хорошо, и работало, как ожидалось.

Глава 2: Возникают новые требования

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

Нашим первым побуждением было то, что, я думаю, сделало бы большинство людей: мы добавили position: sticky к элементу thead таблицы.

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

Мы видим, что элементы таблицы фактически имеют атрибут position: sticky, но после небольшой прокрутки они покидают экран.

Это связано с тем, что согласно спецификациям CSS:

Это значение всегда создает новый контекст стека. Обратите внимание, что закрепленный элемент «прилипает» к своему ближайшему предку, который имеет «механизм прокрутки» (создается, когда переполнение скрыто, прокручивается, автоматически или накладывается), даже если этот предок не является ближайшим фактически прокручиваемым предком.

Итак, проблема здесь:

  • .layout__content прокручивается.
  • .table__wrapper не прокручивается.
  • .table thead липкий по сравнению с .table_wrapper.

Мы хотели, чтобы .table thead было липким по сравнению с .layout__content. Это невозможно, потому что, хотя .table__wrapper имеет только указанное overflow-x, у него все еще есть свойство overflow, поэтому .table thead вместо этого прилипает к нему.

Глава 3: Первый подход

Итак, принимая во внимание характеристики, давайте удалим overflow-x из .layout__content и посмотрим, что получится.

Что же тогда происходит?

Итак, похоже, это сработало… или сработало?

О, нет! Наши кнопки прокрутки перестали работать. Мало того, теперь прокрутка таблицы прокручивает всю страницу, включая заголовок. Этого не должно было случиться.

Но эй! По крайней мере, наши заголовки теперь исправлены!

Глава 4: Не говори мне, что делать!

Итак, после поиска решений (и ломая головы) в течение довольно долгого времени, мы нашли решение для рассматриваемой проблемы: давайте использовать JavaScript и CSS!

Основная идея такова:

  1. Давайте посмотрим, где находится заголовок таблицы.
  2. Давайте посмотрим, как далеко вниз пролистал пользователь. а. Если пользователь прокрутил страницу ниже начала заголовка, давайте translate закрепим элемент thead, чтобы он остался вверху. б. Если нет, удалите свойство translate из заголовка таблицы.

Поскольку мы использовали React для этого приложения, мы использовали некоторые refs для отслеживания фактических элементов DOM.

Код получился примерно таким (я написал его как хук для повторного использования):

Разберем код:

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

Вы можете спросить, почему мы объявляем ссылку на оболочку таблицы, а не на саму таблицу. Причина этого в том, что дальше по коду мы используем некоторое позиционирование для расчета того, сколько пользователь прокрутил, а offsetTop относится к родителю, поэтому таблица всегда имеет offsetTop, равное нулю относительно своего родителя (оболочки). . Обертка должна быть прямым потомком элемента прокрутки (в нашем случае элемента layout__content).

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

Что мы подразумеваем под действовать соответствующим образом? Ну, мы должны проверить положение заголовка относительно элемента прокрутки, но поскольку мы будем его перемещать, мы должны вместо этого проверить положение таблицы, и именно здесь мы используем обёртку таблицы, так как этот элемент не будет перемещаться относительно элемент контента. С этого момента мы можем видеть, прокручивался ли родительский элемент за позицию заголовка, и если это так, мы translate этого элемента на эту разницу позиций прокрутки (scroll position - header position). Если позиция прокрутки выше позиции заголовка, мы просто удаляем это свойство translate.

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

Итак, сработало?

Да! Это не на 100% идеально, заголовок может немного прыгать, но это из-за цикла событий, но это выходит за рамки этого сообщения в блоге.

Но есть кое-что, что всплывает в инструментах разработчика:

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

Ну, Firefox, мы бы использовали position: sticky, если бы он работал так, как мы хотели.

Последние мысли

Работать над этой проблемой было довольно интересно, но я думаю, что было бы неплохо, если бы спецификация CSS учитывала это. Возможно, свойство с именем sticky-anchor со значениями "ancestor" | "screen" было бы неплохо. Я думаю, что ancestor сохранит текущее поведение (и будет значением по умолчанию), а screen будет учитывать только весь экран. Если бы это было так, мы могли бы пропустить весь этот код и просто иметь такой CSS:

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