diff --git a/frontend/src/webparts/approvalAdmin/components/app/TopNav.module.scss b/frontend/src/webparts/approvalAdmin/components/app/TopNav.module.scss index 3704ee4..b741f94 100644 --- a/frontend/src/webparts/approvalAdmin/components/app/TopNav.module.scss +++ b/frontend/src/webparts/approvalAdmin/components/app/TopNav.module.scss @@ -8,7 +8,11 @@ height: 54px; box-shadow: 0 1px 0 rgba(212,160,30,.35); border-radius: $r-md $r-md 0 0; - flex-wrap: wrap; + // Без переноса: высота шапки фиксирована (54px), поэтому перенесённая + // строка вкладок вылезала бы за её пределы вместе с подчёркиванием. + // Вместо переноса вкладки скроллятся по горизонтали. + flex-wrap: nowrap; + overflow: hidden; } .brand { @@ -49,10 +53,27 @@ .tabs { display: flex; height: 100%; - flex-wrap: wrap; + flex-wrap: nowrap; + // min-width: 0 обязателен — иначе flex-элемент не сожмётся уже своего + // содержимого и overflow-x никогда не сработает. + min-width: 0; + overflow-x: auto; + overflow-y: hidden; + // Делает контейнер offsetParent'ом для вкладок: автопрокрутка в TopNav.tsx + // считает offsetLeft относительно него. Без этого offsetLeft отсчитывался + // бы от body и прокрутка уезжала бы не туда. + position: relative; + + // Полоса прокрутки на тёмной шапке выглядит инородно, скрываем её — + // вкладки остаются доступны свайпом/колесом. + scrollbar-width: none; + -ms-overflow-style: none; + &::-webkit-scrollbar { display: none; } } .tab { + flex-shrink: 0; + white-space: nowrap; padding: 0 16px; border: none; background: 0; diff --git a/frontend/src/webparts/approvalAdmin/components/app/TopNav.tsx b/frontend/src/webparts/approvalAdmin/components/app/TopNav.tsx index 2631a44..88f4d28 100644 --- a/frontend/src/webparts/approvalAdmin/components/app/TopNav.tsx +++ b/frontend/src/webparts/approvalAdmin/components/app/TopNav.tsx @@ -19,22 +19,76 @@ export interface ITopNavProps { onSelect: (key: ScreenKey) => void; } -export const TopNav: React.FC = ({ active, onSelect }) => ( - -); +function isTabActive(active: ScreenKey, tabKey: ScreenKey): boolean { + // Экран шагов маршрута — вложенный, подсвечиваем вкладку «Маршруты». + return active === tabKey || (active === 'routeSteps' && tabKey === 'routes'); +} + +export const TopNav: React.FC = ({ active, onSelect }) => { + const tabsRef = React.useRef(null); + const activeTabRef = React.useRef(null); + + // Вкладки скроллятся по горизонтали, поэтому активная может оказаться за + // краем. Подкручиваем контейнер вручную (не scrollIntoView — тот способен + // утащить за собой прокрутку всей страницы SharePoint). + React.useEffect(() => { + const reveal = (): void => { + const container = tabsRef.current; + const tab = activeTabRef.current; + if (!container || !tab) return; + + const left = tab.offsetLeft; + const width = tab.offsetWidth; + + if (width > container.clientWidth) { + // Вкладка шире видимой полосы — показываем её начало, чтобы подпись + // читалась с первого слова. + container.scrollLeft = left; + } else if (left < container.scrollLeft) { + container.scrollLeft = left; + } else if (left + width > container.scrollLeft + container.clientWidth) { + container.scrollLeft = left + width - container.clientWidth; + } + }; + + reveal(); + + // Шрифты (Lora/Inter) подключаются рантаймом: пока они не применились, + // ширины вкладок другие и позиция прокрутки получается устаревшей. + const fonts = (document as Document & { fonts?: FontFaceSet }).fonts; + let cancelled = false; + if (fonts?.ready) { + fonts.ready.then(() => { if (!cancelled) reveal(); }).catch(() => { /* не критично */ }); + } + + window.addEventListener('resize', reveal); + return () => { + cancelled = true; + window.removeEventListener('resize', reveal); + }; + }, [active]); + + return ( + + ); +};