Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 52 additions & 4 deletions frontend/common/hooks/useOverflowVisibleCount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ import { useState, useLayoutEffect, RefObject } from 'react'

const GAP_MULTIPLIER = 5

// offsetWidth rounds to integers; use fractional widths so sums don't
// underestimate and let items clip instead of overflowing
const measureWidth = (el: HTMLElement) => el.getBoundingClientRect().width

type UseOverflowVisibleCountOptions = {
outerContainerRef: RefObject<HTMLDivElement>
itemsContainerRef: RefObject<HTMLDivElement>
Expand Down Expand Up @@ -43,10 +47,33 @@ export const useOverflowVisibleCount = ({
if (!itemsCont) return

const childEls = Array.from(itemsCont.children) as HTMLElement[]
const newWidths = childEls.map((el) => el.offsetWidth)
const newWidths = childEls.map(measureWidth)
setWidths(newWidths)
}, [itemCount, force, widths.length, itemsContainerRef])

// Re-measure when a visible item changes size (e.g. async content such as
// permission-gated links or count badges rendering after the first measure)
useLayoutEffect(() => {
if (force || widths.length === 0) {
return
}

const itemsCont = itemsContainerRef.current
if (!itemsCont) return

const ro = new ResizeObserver(() => {
const childEls = Array.from(itemsCont.children) as HTMLElement[]
const changed = childEls.some(
(el, i) => Math.abs(measureWidth(el) - widths[i]) > 0.5,
)
if (changed) {
setWidths([])
}
})
Array.from(itemsCont.children).forEach((el) => ro.observe(el))
return () => ro.disconnect()
}, [widths, force, itemsContainerRef])

Comment thread
kyle-ssg marked this conversation as resolved.
Outdated
// Calculate visible count based
useLayoutEffect(() => {
if (force || widths.length === 0) {
Expand All @@ -57,7 +84,15 @@ export const useOverflowVisibleCount = ({
const outerCont = outerContainerRef.current
if (!outerCont) return

const gapPx = gap * GAP_MULTIPLIER
// Read the real flex gap; the gap * GAP_MULTIPLIER guess undershoots
// (e.g. gap-3 is 16px, not 15px) which lets items clip off-screen
const itemsCont = itemsContainerRef.current
const computedGap = itemsCont
? parseFloat(getComputedStyle(itemsCont).columnGap)
: NaN
const gapPx = Number.isNaN(computedGap)
? gap * GAP_MULTIPLIER
: computedGap

const sumWidths = (count: number) => {
if (count === 0) return 0
Expand All @@ -68,7 +103,12 @@ export const useOverflowVisibleCount = ({
return totalWidths + totalGaps
}

const containerWidth = outerCont.clientWidth
// clientWidth includes padding, which is not available to items
const outerStyle = getComputedStyle(outerCont)
const containerWidth =
outerCont.clientWidth -
(parseFloat(outerStyle.paddingLeft) || 0) -
(parseFloat(outerStyle.paddingRight) || 0)
// All items fit
if (sumWidths(widths.length) <= containerWidth) {
setVisibleCount(widths.length)
Expand All @@ -93,7 +133,15 @@ export const useOverflowVisibleCount = ({
ro.observe(outerContainerRef.current)
}
return () => ro.disconnect()
}, [widths, force, gap, itemCount, extraWidth, outerContainerRef])
}, [
widths,
force,
gap,
itemCount,
extraWidth,
outerContainerRef,
itemsContainerRef,
])

const isMeasuring = !force && widths.length === 0 && itemCount > 0

Expand Down
9 changes: 8 additions & 1 deletion frontend/web/components/navigation/OverflowNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,15 @@ const OverflowNav: FC<OverflowNavProps> = ({
'd-flex align-items-center',
)}
>
{/* Wrappers keep DOM children 1:1 with items so measured widths align,
even when an item renders nothing or multiple elements */}
{(isMeasuring ? items : visible).map((child, idx) => (
<React.Fragment key={idx}>{child}</React.Fragment>
<div
key={idx}
className={`d-flex align-items-center gap-${gap} flex-shrink-0`}
>
{child}
</div>
))}
</div>

Expand Down
36 changes: 17 additions & 19 deletions frontend/web/components/navigation/navbars/ProjectNavbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ const ProjectNavbar: FC<ProjectNavType> = ({ environmentId, projectId }) => {
location.pathname.startsWith(`/project/${projectId}/lifecycle`)
}
>
Feature Lifecycle
Lifecycle
</NavSubLink>
)}
<Permission
Expand Down Expand Up @@ -114,25 +114,23 @@ const ProjectNavbar: FC<ProjectNavType> = ({ environmentId, projectId }) => {
>
Compare
</NavSubLink>
{projectAdmin && Utils.getFlagsmithHasFeature('release_pipelines') && (
<NavSubLink
icon={<Icon name='flash' />}
id='release-pipelines-link'
to={`/project/${projectId}/release-pipelines`}
>
Pipelines
</NavSubLink>
)}
{projectAdmin && (
<>
{Utils.getFlagsmithHasFeature('release_pipelines') && (
<NavSubLink
icon={<Icon name='flash' />}
id='release-pipelines-link'
to={`/project/${projectId}/release-pipelines`}
>
Release Pipelines
</NavSubLink>
)}
<NavSubLink
icon={<Icon name='setting' width={24} />}
id='project-settings-link'
to={`/project/${projectId}/settings`}
>
Project Settings
</NavSubLink>
</>
<NavSubLink
icon={<Icon name='setting' width={24} />}
id='project-settings-link'
to={`/project/${projectId}/settings`}
>
Project Settings
</NavSubLink>
)}
</OverflowNav>
)
Expand Down
Loading