Привет, Хабр!
Начать данную статью хотелось бы с небольшого лирического отступления. Недавно, в очередной раз столкнувшись со сложным кейсом на работе и прошерстив добрую половину интернета в поисках истины, попутно приобретя с десяток седых волос, у меня начало болеть сердечко за начинающих специалистов, которые выбрали нелегкий путь разработчика JS. Как вы, вероятно, догадались, сам я не отношусь к категории людей ‘В детстве мама включала мне вместо мультиков курсы Javascript для самых маленьких, в 5 я написал свой интернет магазин,а в 10 в первый раз взломал сайт Пентагона’. Удивительно, но на похожие резюме я не раз натыкался на просторах интернета, те самые 25-ти летние специалисты с 22-х летним опытом на самом деле существуют и, наверное, не имеют тех проблем, с которыми приходится сталкиваться мне — человеку, которому разработка даётся довольно нелегко и который до недавнего времени считал, что нормальный код могут писать только люди со сверхразумом и поэтому боялся даже попробовать. Я считаю, что таких как я на самом деле очень много, особенно учитывая безумный рост популярности программирования (и Javascript в частности) в широких массах в последнее время. Поэтому решил периодически публиковать здесь свои заметки и кейсы, с которыми я сталкивался в работе и которые вызывали у меня определенные трудности, чтобы несколько облегчить жизнь разработчикам, встречающим схожие проблемы.
P.S. я не претендую на экспертность знаний, поэтому, с определенной вероятностью могут найтись куда более оптимальные решения. Помните: ‘Я художник — я так вижу’.
А теперь плавно переходим к теме статьи. Ниже приведен список вопросов, которые я постараюсь осветить в данной заметке:
-
Как сделать таблицу с фиксированной шапкой и скроллом в body?
-
Как быть, когда cодержимое таблицы съезжает относительно шапки при появлении скролла? Установка css-переменной scroll-width.
-
Оптимизация и кастомизация скролла: плавность, scroll margin, изменение цвета и формы.
Как сделать таблицу с фиксированной шапкой и скроллом в body?
Начнем с того, что сделаем самую обыкновенную табличку и заполним ее данными из faker для наглядности. Получаем примерно такую картину(верстку делаем самую базовую, просто чтобы глаз не дергался от одного ее вида):
Код
import React from 'react'; import { render } from 'react-dom'; import faker from 'faker'; import './index.css'; // генерируем данные const users = Array(100).fill(null).map(() => ({ id: faker.random.uuid(), first: faker.name.firstName(), last: faker.name.lastName(), email: faker.internet.email(), })); const App = () => ( <table > <thead> <tr> <th>First</th> <th>Last</th> <th>Email</th> </tr> </thead> <tbody> {users.map(x => ( <tr key={x.id}> <td>{x.first}</td> <td>{x.last}</td> <td>{x.email}</td> </tr> ))} </tbody> </table> ); render(<App />, document.getElementById('root'));
table { width: 100%; border-collapse: collapse; border-spacing: 0; } thead { background-color: teal; } td, th { padding: 15px; text-align: left; } th { color: white; }
Как мы видим, по умолчанию снаружи у нас появился скролл, но нам такая большая таблица не нужна, по условию мы хотим, чтобы скролл у нас был только у тела таблицы, а header над всем этим делом нависал сверху, ну и чтобы в высоту наша табличка была не больше 200 пикселей. В общем, что-то наподобие вот этого:
Нужного нам результата можно добиться несколькими способами:
-
Использовать библиотеку по типу React-Table, React-Virtualized, либо какую-нибудь еще, чтобы вообще не заморачиваться. Настоятельно рекоммендую хотя бы с одной из них ознакомиться, поскольку они очень значительно облегчают жизнь разработчика при работе с таблицами.
-
Немного подшаманить CSS, чем мы сейчас и займемся
Первое,что мне лично пришло на ум, когда я в первый раз столкнулся с подобной задачей — нужно всего лишь поменять свойство display у тегов tbody и thead на ‘block’ и накинуть на tbody ограничение по высоте (здесь я считаю необходимым прояснить небольшой момент, вызывающий трудности у начинающих: многие пытаются просто задать высоту для body, а потом не могут понять, почему это не работает. Нужно просто не забыть еще прописать свойство overflow, и все получится).
Зачем нам менять display? Дело в том, что свойство display у элементов thead и tbody имеет значение row-group(table-header-group и table-row-group соответственно), что в свою очередь не позволяет нам вводить ограничение по высоте, а значит и скролла у них не будет, поэтому мы меняем их на понятный нам display: ‘block’, которым проще управлять .
Напоминалка про поведение атрибута height в таблицах
Когда в документе задан <!DOCTYPE>, браузеры игнорируют высоту таблицы, заданную через атрибут height. По умолчанию высота вычисляется на основе содержимого таблицы.
Здесь есть несколько выжных моментов, о которых не стоит забывать:
-
Меняя свойство display на ‘block’, мы как бы ломаем семантику, и наш браузер уже больше не считает нашу таблицу таблицей, что также оказывает влияние на скринридеры.
-
Теперь наш тег tr уже не занимает все свободное пространство контейнера и теперь нам нужно все это дело выровнять.
Код
import React from 'react'; import { render } from 'react-dom'; import faker from 'faker'; import './index.css'; // генерируем данные const users = Array(100).fill(null).map(() => ({ id: faker.random.uuid(), first: faker.name.firstName(), last: faker.name.lastName(), email: faker.internet.email(), })); const App = () => ( <table > <thead> <tr> <th>First</th> <th>Last</th> <th>Email</th> </tr> </thead> <tbody> {users.map(x => ( <tr key={x.id}> <td>{x.first}</td> <td>{x.last}</td> <td>{x.email}</td> </tr> ))} </tbody> </table> ); render(<App />, document.getElementById('root'));
table { width: 100%; border-collapse: collapse; border-spacing: 0; } thead { display: block; background-color: teal; } tbody { display: block; overflow-y: auto; overflow-x: hidden; max-height: 200px; min-height: 100px; } tr { border: 1px solid black; } td, th { text-align: left; padding: 15px; } th { color: white; }
Как будем выравнивать?
В общем, в любой непонятной ситуации используйте Flex или Grid и будет вам счастье. Ниже приведу два готовых базовых примера, html трогать не буду, но я думаю, что и так понятно, что можно сразу верстать на div и маркированных списках при желании. Дальше уже по ситуации можете довести до ума верстку, например: добавить ограничение таблицы по ширине, нужный вам формат переноса строк и адаптив, если конечно он нужен, так как все же в большинстве своем контент таблиц предназначен для использования на больших экранах. В любом случае, более продвинутая верстка легко гуглится.
Готовый вариант с display: ‘Flex’
import React from 'react'; import { render } from 'react-dom'; import faker from 'faker'; import './index.css'; // генерируем данные const users = Array(100).fill(null).map(() => ({ id: faker.random.uuid(), first: faker.name.firstName(), last: faker.name.lastName(), email: faker.internet.email(), })); const App = () => ( <table className='table'> <thead> <tr className='header'> <th className='firstColumn'>First</th> <th className='secondColumn'>Last</th> <th className='thirdColumn'>Email</th> </tr> </thead> <tbody> {users.map(x => ( <tr className='body' key={x.id}> <td className='firstColumn'>{x.first}</td> <td className='secondColumn'>{x.last}</td> <td className='thirdColumn'>{x.email}</td> </tr> ))} </tbody> </table> ); render(<App />, document.getElementById('root'));
table { width: 100%; border-collapse: collapse; border-spacing: 0; } thead { background-color: teal; color: white; } thead, tbody, th, td { display: block; text-align: left; } tbody { overflow-y: auto; overflow-x: hidden; max-height: 200px; min-height: 100px; } tr { display: flex; } .firstColumn, .secondColumn { flex: 1; } .thirdColumn { flex: 3; } td, th { padding: 15px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
Готовый вариант с display ‘Grid’
import React from 'react'; import { render } from 'react-dom'; import faker from 'faker'; import './index.css'; // генерируем данные const users = Array(100).fill(null).map(() => ({ id: faker.random.uuid(), first: faker.name.firstName(), last: faker.name.lastName(), email: faker.internet.email(), })); const App = () => ( <table className='table'> <thead> <tr className='header'> <th>First</th> <th>Last</th> <th>Email</th> </tr> </thead> <tbody> {users.map(x => ( <tr className='body' key={x.id}> <td>{x.first}</td> <td>{x.last}</td> <td>{x.email}</td> </tr> ))} </tbody> </table> ); render(<App />, document.getElementById('root'));
table { width: 100%; border-collapse: collapse; border-spacing: 0; } tr { display: grid; grid-template-columns: 1fr 1fr 2fr; } thead { background-color: teal; color: white; } thead, tbody, th, td { display: block; text-align: left; } tbody { overflow-y: auto; overflow-x: hidden; max-height: 200px; min-height: 100px; } td, th { padding: 15px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
Чуть не забыл отметить одну очень важную вещь, с которой я в свое время достаточно долго пытался разобраться: НА МАКАХ ЕСТЬ ФУНКЦИЯ АВТОМАТИЧЕСКОГО СКРЫТИЯ СКРОЛЛБАРА В БРАУЗЕРАХ, КОТОРАЯ ВЫКЛЮЧАЕТСЯ В НАСТРОЙКАХ. Если вы выставляете overflow: scroll, и ждете, что scroll теперь будет виден постоянно, то на маке он по умолчанию может пропадать и быть видимым только в момент прокрутки. Плюс на маках скролл у нас как бы парит в воздухе над контентом, аналогично position: absolute и не занимает места, если соответствующая настройка включена (по умолчанию она всегда включена).
Да-да, получается что внешний вид скролла зависит также от операционной системы, а не только от браузера.
Глядя на результат для windows, мы плавно подходим к следующей теме.
Как быть, когда cодержимое таблицы съезжает относительно шапки при появлении скролла? Установка css-переменной scroll-width
Мы отчетливо видим, что на windows скролл при появлении съедает часть пространства, что в свою очередь приводит к тому, что контент нашей таблицы съезжает относительно шапки. Это лишь один из примеров, но есть и другие кейсы, когда скролл негативно влияет на качество верстки. Казалось бы, проблема очевидная, и наверняка должен быть способ как-то с этим бороться, однако, к сожалению, на данный момент есть только костыльные решения (во всяком случае я не нашел того, что бы меня устроило). Ниже я расскажу о самом распространенном варианте решения данной проблемы, плюс затрону относительно новое CSS свойство, которое должно потенциально избавить разработчиков от этой головной боли.
Ну что ж, приступим! Алгоритм прост и содержит всего лишь 2 шага:
-
Задаем для overflow-y значение scroll, чтобы в любом состоянии скролл у нас присутствовал в таблице (отмечу, что часто дизайнеры не в восторге от такого поворота событий, не по фэншую это всё и всю красоту портит).
-
Высчитываем ширину скролла в браузере и двигаем шапку на нужное расстояние.
Как вы уже догадались из заголовка, значение ширины скролла очень удобно хранить в css переменной, чтобы иметь к нему доступ в любом месте в приложении. Ниже я расскажу, как это сделать
Как посчитать размер скролла и положить его в css-переменную?
Я приведу лишь один из методов, которым пользуюсь сам, но вы также можете воспользоваться для этих целей либо одним из готовых решений, либо хотя бы посмотреть, как именно они реализованы и сделать на их основе свое. Принцип везде один и тот же.
const setScrollbarWidth = () => { // Создаем контейнер-болванку const outerContainer = document.createElement('div'); //Накидываем стили outerContainer.style.visibility = 'hidden'; outerContainer.style.overflow = 'scroll'; //Добавляем его к body document.body.appendChild(outerContainer); // Создаем еще один контейнер и помещаем его внутрь outerContainer const innerContainer = document.createElement('div'); outerContainer.appendChild(innerContainer); // Высчитываем ширину нашего скроллбара const scrollbarWidth = (outerContainer.offsetWidth - innerContainer.offsetWidth); // Создаем css-переменную document.documentElement.style.setProperty('--scroll-width', `${scrollbarWidth}px`); // Удаляем наш контейнер-болванку outerContainer.parentNode.removeChild(outerContainer); }
Теперь нам остается только вызвать функцию при загрузке приложения и использовать значение ширины скролла в нужном нам месте. Если кто-то ранее не сталкивался с css-переменными, то можете поискать на хабре, тут есть неплохие статьи на эту тему с большим кол-вом примеров. Использовать css-переменные очень просто:
padding-right: var(--scroll-width)
Применяя все вышеописанное к моему изначальному коду получаем:
Код
import React, { useEffect } from "react"; import { render } from "react-dom"; import faker from "faker"; import "./index.css"; const setScrollbarWidth = () => { const outerContainer = document.createElement("div"); outerContainer.style.visibility = "hidden"; outerContainer.style.overflow = "scroll"; document.body.appendChild(outerContainer); const innerContainer = document.createElement("div"); outerContainer.appendChild(innerContainer); const scrollbarWidth = outerContainer.offsetWidth - innerContainer.offsetWidth; document.documentElement.style.setProperty( "--scroll-width", `${scrollbarWidth}px` ); outerContainer.parentNode.removeChild(outerContainer); }; // генерируем данные const users = Array(100) .fill(null) .map(() => ({ id: faker.random.uuid(), first: faker.name.firstName(), last: faker.name.lastName(), email: faker.internet.email() })); const App = () => { useEffect(() => setScrollbarWidth(), []); return ( <table className="table"> <thead> <tr className="header"> <th>First</th> <th>Last</th> <th>Email</th> </tr> </thead> <tbody> {users.map((x) => ( <tr className="body" key={x.id}> <td>{x.first}</td> <td>{x.last}</td> <td>{x.email}</td> </tr> ))} </tbody> </table> ); }; render(<App />, document.getElementById("root"));
table { width: 100%; border-collapse: collapse; border-spacing: 0; } tr { display: grid; grid-template-columns: 1fr 1fr 2fr; } thead { padding-right: var(--scroll-width); background-color: teal; color: white; } thead, tbody, th, td { display: block; text-align: left; } tbody { overflow-y: scroll; overflow-x: hidden; max-height: 200px; min-height: 100px; } td, th { padding: 15px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
В очередной раз хочу напомнить, что на маке ничего съезжать не будет по умолчанию, но если уж вы включите скролл в настройках, то предложенное мной решение также отлично отработает и всё будет ровненько.
Раньше было еще одно решение данной проблемы — использование overflow в значении overlay, что позволяло сделать скроллбар примерно таким же, как сейчас на маках, т.е. не занимающим места (поверх контента), однако это свойство устаревшее и использовать его — это очень плохая идея.
Можно ли каким-то образом все-таки избежать использования overflow: scroll?
Ответ: в данной ситуации можно, нужно всего лишь немного усовершенствовать наш костыль.
Алгоритм действий:
-
Объявляем отдельный класс с нашим padding
-
Меняем overflow на auto (чтобы скролл появлялся только по необходимости)
-
При загрузке страницы определяем ширину шапки и body
-
Используем classnames, чтобы установить условие на добавление нашего новго класса к шапке: ширина body не должна быть равна ширине шапки (то есть мы добавляем сдвиг, только если в body есть scroll). Для удобства храним результат проверки в стэйте.
Финальный код
import React, { useEffect, useState } from "react"; import { render } from "react-dom"; import faker from "faker"; import cx from "classnames"; import "./index.css"; const setScrollbarWidth = () => { const outerContainer = document.createElement("div"); outerContainer.style.visibility = "hidden"; outerContainer.style.overflow = "scroll"; document.body.appendChild(outerContainer); const innerContainer = document.createElement("div"); outerContainer.appendChild(innerContainer); const scrollbarWidth = outerContainer.offsetWidth - innerContainer.offsetWidth; document.documentElement.style.setProperty( "--scroll-width", `${scrollbarWidth}px` ); outerContainer.parentNode.removeChild(outerContainer); }; // генерируем данные const users = Array(30) .fill(null) .map(() => ({ id: faker.random.uuid(), first: faker.name.firstName(), last: faker.name.lastName(), email: faker.internet.email() })); const App = () => { const [isShiftAllowed, setIsShiftAllowed] = useState(false); useEffect(() => { setScrollbarWidth(); const headerWidth = document.querySelector(".header").clientWidth; const bodyWidth = document.querySelector(".body").clientWidth; headerWidth - bodyWidth !== 0 ? setIsShiftAllowed(true) : setIsShiftAllowed(false); }, []); return ( <table className="table"> <thead className={cx({ shift: isShiftAllowed })}> <tr className="header"> <th>First</th> <th>Last</th> <th>Email</th> </tr> </thead> <tbody> {users.map((x) => ( <tr className="body" key={x.id}> <td>{x.first}</td> <td>{x.last}</td> <td>{x.email}</td> </tr> ))} </tbody> </table> ); }; render(<App />, document.getElementById("root"));
table { width: 100%; border-collapse: collapse; border-spacing: 0; } .shift { padding-right: var(--scroll-width); } tr { display: grid; grid-template-columns: 1fr 1fr 2fr; } thead { background-color: teal; color: white; } thead, tbody, th, td { display: block; text-align: left; } tbody { overflow-y: auto; overflow-x: hidden; max-height: 200px; min-height: 100px; } td, th { padding: 15px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
Если кто-нибудь знает, как можно всё это сделать более оптимальным способом, то пожалуйста дайте мне об этом знать в комментариях.
Я упоминал о том, что есть новое css свойство, которое может стать настоящим спасением в данном вопросе, имя ему scrollbar-gutter. Еще раз повторюсь, это новое свойство, и на данный момент оно поддерживается только браузерами Opera, Chrome и Edge, так сказать still work in progress. Как объясняется в MDN, scrollbar gutter — это собственно и есть контейнер, в котором лежит наш бегунок скроллбара (если конечно overflow не находится в значении overlay). Соответственно, комбинирование значений данного свойства со значениями свойства overflow позволит гибко управлять поведением этого контейнера, резервировать место и т.д. Очень надеюсь, что в ближайшем будущем его наконец полноценно допилят.
Заметка получилась уже довольно большой, но у нас осталась еще одна тема, потерпите еще немного, постараюсь написать чего-нибудь интересненького.
Оптимизация и кастомизация скролла.
Большую часть информации для данного раздела я подчеркнул из статей, автором которых является Josh W Comeau, всем рекоммендую посетить его вебсайт https://www.joshwcomeau.com/, там вы найдете очень много полезностей.
Добавляем плавность
Возможно, глядя на заголовок, кто-то из вас сейчас сидит в недоумении: ‘Плавность? Скролл ведь и так плавный, о чем ты?’. Так-то оно так, но я вот о что имею ввиду: все мы прекрасно знаем о том, что в html можно сделать ссылку-якорь, чтобы при клике на заголовок перемещаться к нужному фрагменту текста, самый базовый пример можно найти на всеми любимой википедии:
Как мы видим, перемещение происходит мгновенно, однако мы можем сделать этот переход плавным с помощью CSS (возможно это для вас конечно не новость, но кому-то точно будет полезно). Для этого нам нужно использовать вот этот кусочек кода:
@media (prefers-reduced-motion: no-preference) { html { scroll-behavior: smooth; } }
Теперь то же действие на страничке википедии происходит таким образом:
Кстати говоря, если вдруг кто-то не знал, в Chrome devtools можно также добавлять media queries. Вот здесь можно найти подробную инструкцию о том, как это сделать: https://daveceddia.com/inspector-stylesheet-chrome/. Кейс супер редкий, но мало ли, вдруг понадобится.
На мой взгляд более плавное перемещение при клике на ссылку-якорь выглядит куда приятнее и позволяет лучше ориентироваться на странице, да и поддерживается достаточно хорошо, не работает лишь в Internet Explorer и SAFARI.
Отмечу также важный момент: если вы добавляете scroll-behavior: smooth в html, то имейте ввиду, что это автоматически также аффектит поведение метода window.scrollTo (тоже становится плавным), который используется, например, для того чтобы переместить пользователя наверх страницы в ответ на submit. Если вдруг вы раньше не сталкивались с этим методом, то обязательно ознакомьтесь и также не забудьте про метод scrollIntoView (разница между ними в том, что первый оперирует пикселями, а второй перемещает к конкретному элементу).
Scroll margin
Как мы видели на примере выше, при клике на ссылку-якорь наша страничка прокручивается таким образом, что нужный нам текст оказывается в самом верху экрана. Однако, если у нас имеется header со свойством position в значении sticky или fixed, то часть нужного нам контента обязательно заползет под него и нам придется после этого скроллить страничку вверх, чтобы этот самый контент стало видно.
Здесь нам поможет использование свойства scroll-margin-top, которое определяет, где элемент должен находиться относительно верхней части окна при прокрутке. Со значением нужно поэкспериментировать и подобрать подходящее под вашу конкретную ситуацию.
Изменение цвета.
Иногда стандартный скролл очень сильно выбивается из общей картины сайта и портит все впечатление, поэтому, зачастую, встает вопрос о том, каким образом можно его хотя бы немного кастомизировать.
Возможно вы уже сталкивались со свойтвом scrollbar-color, которое позволяет изменить цвет скроллбара, только вот к сожалению поддерживается оно только браузером Firefox.
body { scrollbar-color: color1 color2; }
Это конечно здорово, но сразу возникает вопрос: ‘Есть ли альтернатива для остальных браузеров? ‘ Альтернатива есть и имя ей: Vendor-prefixes.
::-webkit-scrollbar { /* Цвет контейнера */ background-color: color2; } ::-webkit-scrollbar-thumb { /* Цвет ползунка */ background-color: color1; }
Однако, стоит отметить, что если мы изменим цвет таким образом, то результат будет достаточно топорным для остальных браузеров, так как мы затираем стандартные свойства скроллбара. Вот вам для примера сравнение результата в браузерах Chrome и Firefox:
Чтобы сделать картинку получше и добиться примерно одинакового внешнего вида в разных браузерах, нам нужно накинуть дополнительных стилей (на наше счастье псевдоэлемент -webkit-scrollbar позволяет нам это сделать):
<style> html { --background: hsl(210deg, 15%, 6.25%); --text: hsl(210deg, 10%, 90%); --gray-300: hsl(210deg, 10%, 40%); --gray-500: hsl(210deg, 8%, 50%); /* Official styles (Firefox) */ scrollbar-color: var(--gray-300) var(--background); scrollbar-width: thin; } ::-webkit-scrollbar { width: 10px; background-color: var(--background); } ::-webkit-scrollbar-thumb { border-radius: 1000px; background-color: var(--gray-300); border: 2px solid var(--background); } /* Little bonus: on non-Firefox browsers, the thumb will light up on hover! */ ::-webkit-scrollbar-thumb:hover { background-color: var(--gray-500); } </style>
Ну вот и все, друзья мои, я рассказал вам обо всех моментах, которые раньше у меня вызывали определенные трудности. Данная заметка — это определенно всего лишь ‘Капля в море’ и у вас 100% найдется еще большое количество вопросов и разного рода уточнений, a также замечаний, надеюсь увидеть их в комментариях (я пока еще учусь, так что не судите строго).
В дальнейшем попробую еще написать набольшую заметку про 2 другие интересные темы, касающиеся скролла: Cumulative Layout Shifts и Scroll Snapping.
Очень надеюсь, что вам было хоть немного полезно и интересно!
ссылка на оригинал статьи https://habr.com/ru/post/645259/
Добавить комментарий