Главная » Статьи » Фиксированный заголовок с выделенными разделами при прокрутке

Фиксированный заголовок с выделенными разделами при прокрутке

Фиксированный заголовок с выделенными разделами при прокрутке

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

Раньше до добавления CSS position: sticky реализация фиксированного заголовка была огромной болью. Поддержка некоторых сценариев (таких как заголовки таблиц и т. д.) была на низком уровне, но вы можете узнать больше об этом на веб-сайте caniuse.

Настройка

Это будет структура, с которой мы будем работать. Некоторые вещи, которые стоит упомянуть — это top-spacer и bottom-spacer, просто произвольные высоты. Это позволяет нам иметь пространство для прокрутки сверху и достаточно места, чтобы прокручивать раздел контента полностью.

import React, { useRef, useEffect, useState } from "react";
import "./App.css"; function App() { return ( <div className="App"> <div className="top-spacer" /> <div className="content"> <div className="sticky"> <div className="header"> <button type="button" className="header_link"> Leadership </button> <button type="button" className="header_link"> Providers </button> <button type="button" className="header_link"> Operations </button> </div> </div> <div className="section" id="Leadership" /> <div className="section" id="Providers" /> <div className="section" id="Operations" /> </div> <div className="bottom-spacer" /> </div> );
} export default App;

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

Модель header_link отображает прозрачную обводку размером 3 пикселя, потому что когда мы применяем класс selected вместе с зеленой нижней обводкой размером 3 пикселя, это не приведет к тому, что текст перескочит и выйдет за пределы другого текста.

.top-spacer { height: 50vh;
} .bottom-spacer { height: 110vh;
} .header { display: flex; justify-content: center; background-color: #fff;
} .header_link { padding: 20px; color: #314d4a; font-weight: bold; border: none; border-bottom: 3px solid transparent; cursor: pointer; outline: none;
} .selected { border-bottom: 3px solid #11bb9a; color: #11bb9a;
} .section { height: 40vh;
} #Leadership { background-color: #a388e8;
} #Providers { background-color: #f4769e;
} #Operations { background-color: #8face0;
}

Сделаем заголовок фиксированным

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

.sticky { position: sticky; top: 0; left: 0; right: 0; z-index: 10;
}

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

<div className="content"> <div className="sticky" /> {/* articles, other content we care about */}
</div>

Фиксированный заголовок с выделенными разделами при прокрутке

Клик для прокрутки

Браузер также реализует возможность прокрутки к различным элементам, вы можете вызвать element.scrollIntoView() или элемент scrollTo для прокрутки до определенного смещения.

Мы будем использовать element.scrollIntoView(), поскольку у нас есть ссылки на все разделы, к которым мы можем прокрутить контент. Мы определяем поведение для плавной прокрутки к начальной позиции элемента.

const scrollTo = ele => { ele.scrollIntoView({ behavior: "smooth", block: "start", });
};

Мы вернемся к этому позже, но у каждого раздела будет свой ref, чтобы мы могли измерить высоту. Но мы также можем использовать эти ref для элементов DOM, чтобы получить их offsetTop и прокрутить до них.

<div className="header"> <button type="button" className="header_link" onClick={() => { scrollTo(leadershipRef.current); }} > Leadership </button> <button type="button" className="header_link" onClick={() => { scrollTo(providerRef.current); }} > Providers </button> <button type="button" className="header_link" onClick={() => { scrollTo(operationsRef.current); }} > Operations </button>
</div>

Фиксированный заголовок с выделенными разделами при прокрутке

Прокрутка в пределах монитора

Нам нужно знать, куда пользователь в данный момент прокрутил страницу, а также получать уведомления о прокрутке. Мы можем сделать это, подключив прослушиватель scroll к window. Мы прикрепляем его к window, потому что это то, что прокручивается, если прокручивается альтернативный элемент, вы могли бы прикрепить свойство onScroll.

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

При работе с хуками, которые будут зависеть от чистоты или зависимостей, лучше всего создавать функцию внутри хука useEffect.

useEffect(() => { const handleScroll = () => {}; window.addEventListener("scroll", handleScroll); return () => { window.removeEventListener("scroll", handleScroll); };
}, []);

Измерение высоты и смещения

Мы будем использовать хук useRef, который позволит получить доступ к элементу DOM каждого раздела, чтобы мы могли получить высоту / смещение. Таким образом, у нас есть значение для ссылки на будущее, мы можем поместить их в массив, поэтому, когда мы обнаруживаем что-то имеющее отношение к нашему ref, у нас есть способ определить, о чем идет речь.

const leadershipRef = useRef(null);
const providerRef = useRef(null);
const operationsRef = useRef(null); const sectionRefs = [ { section: "Leadership", ref: leadershipRef }, { section: "Providers", ref: providerRef }, { section: "Operations", ref: operationsRef },
];

Теперь мы прикрепляем их к разделам.

<div className="section" id="Leadership" ref={leadershipRef} />
<div className="section" id="Providers" ref={providerRef} />
<div className="section" id="Operations" ref={operationsRef} />

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

Концепция заключается в том, чтобы получить scrollY (на сколько пользователь прокрутил страницу). Затем мы можем увидеть, находится ли это значение между верхом / низом элемента.

Мы используем getBoundingClientRect(), чтобы получить height элемента. Тогда offsetTop будет верхней позицией в пикселях. Внизу будет offsetTop + height элемента.

const getDimensions = ele => { const { height } = ele.getBoundingClientRect(); const offsetTop = ele.offsetTop; const offsetBottom = offsetTop + height; return { height, offsetTop, offsetBottom, };
};

Выделение раздела при прокрутке

Теперь, когда мы отслеживаем положение прокрутки и знаем позицию верха и низа каждого из разделов, мы можем определить, который из них просматривает пользователь и выделить его.

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

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

const [visibleSection, setVisibleSection] = useState();
const headerRef = useRef(null);

Прикрепляем ref.

<div className="header" ref={headerRef}>

Сначала мы получим высоту заголовка, у нас уже есть функция, которая будет использовать height элемента, чтобы мы могли вызвать ее и деструктурировать. Нашей фактической позицией прокрутки, о которой мы заботимся, будет scrollY окна плюс дополнительная высота заголовка.

const handleScroll = () => { const { height: headerHeight } = getDimensions(headerRef.current); const scrollPosition = window.scrollY + headerHeight;
};

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

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

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

const selected = sectionRefs.find(({ section, ref }) => { const ele = ref.current; if (ele) { const { offsetBottom, offsetTop } = getDimensions(ele); return scrollPosition > offsetTop && scrollPosition < offsetBottom; }
}); if (selected && selected.section !== visibleSection) { setVisibleSection(selected.section);
}

Стоит отметить, что, поскольку мы зависим от предыдущего visibleSection, нам нужно добавить его в зависимости useEffect. Когда раздел изменится, он запустит очистку, удалит прослушиватель прокрутки окна и затем снова запустит эффект.

useEffect(() => { const handleScroll = () => { const { height: headerHeight } = getDimensions(headerRef.current); const scrollPosition = window.scrollY + headerHeight; const selected = sectionRefs.find(({ section, ref }) => { const ele = ref.current; if (ele) { const { offsetBottom, offsetTop } = getDimensions(ele); return scrollPosition > offsetTop && scrollPosition < offsetBottom; } }); if (selected && selected.section !== visibleSection) { setVisibleSection(selected.section); } }; window.addEventListener("scroll", handleScroll); return () => { window.removeEventListener("scroll", handleScroll); };
}, [visibleSection]);

Наконец, поскольку у нас есть обновление состояния visibleSection при прокрутке, мы можем применить класс selected, чтобы добавить зеленую обводку внизу и зеленый текст, чтобы указать пользователям, в каком разделе они находятся.

<div className="header"> <button type="button" className={`header_link ${visibleSection === "Leadership" ? "selected" : ""}`} onClick={() => { scrollTo(leadershipRef.current); }} > Leadership </button> <button type="button" className={`header_link ${visibleSection === "Providers" ? "selected" : ""}`} onClick={() => { scrollTo(providerRef.current); }} > Providers </button> <button type="button" className={`header_link ${visibleSection === "Operations" ? "selected" : ""}`} onClick={() => { scrollTo(operationsRef.current); }} > Operations </button>
</div>

Фиксированный заголовок с выделенными разделами при прокрутке

Удаление предыдущего выделения

Еще один момент, о котором нам нужно позаботиться — когда пользователь прокручивает контент вниз, а затем прокручивает обратно в начало страницы. Если мы добавим else if и обнаружим, что мы не нашли раздел selected. Это означает, что мы вообще не находимся ни в одном разделе. Затем мы проверяем, есть ли у нас выбранный раздел, и удаляем выделение, установив для visibleSection значение undefined.

if (selected && selected.section !== visibleSection) { setVisibleSection(selected.section);
} else if (!selected && visibleSection) { setVisibleSection(undefined);
}

Восстановление позиции прокрутки

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

useEffect(() => { const handleScroll = () => { // Scroll Code }; handleScroll(); window.addEventListener("scroll", handleScroll); return () => { window.removeEventListener("scroll", handleScroll); };
}, [visibleSection]);

Заключение

Это все. Если вы просто хотели закрепить заголовок и вам не нужно знать, какой раздел вы сейчас просматриваете, вы могли бы просто добавить position: sticky. Но возможность нажимать кнопки для прокрутки к разделам и выделение раздела, в котором находится пользователь, обеспечивает более приятный опыт.

Фиксированный заголовок с выделенными разделами при прокрутке

Автор: Jason Brown

Источник: https://codeburst.io

Редакция: Команда webformyself.