Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
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
66 changes: 62 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,43 @@ 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

// Coalesce a burst of resize entries into a single re-measure per frame,
// and ignore sub-pixel drift so fractional rounding doesn't churn
let frame = 0
const ro = new ResizeObserver(() => {
if (frame) return
frame = requestAnimationFrame(() => {
frame = 0
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 () => {
if (frame) cancelAnimationFrame(frame)
ro.disconnect()
}
}, [widths, force, itemsContainerRef])

// Calculate visible count based
useLayoutEffect(() => {
if (force || widths.length === 0) {
Expand All @@ -57,7 +94,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 +113,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 +143,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
37 changes: 18 additions & 19 deletions frontend/web/components/navigation/navbars/OrganisationNavbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,28 +42,27 @@ const OrganisationNavbar: FC<OrganisationNavType> = ({}) => {
Usage
</NavSubLink>
)}
{AccountStore.isAdmin() && (
<>
{Utils.getFlagsmithHasFeature('organisation_integrations') && (
<NavSubLink
icon={<Icon name='layers' />}
id='integrations-link'
to={`/organisation/${
AccountStore.getOrganisation().id
}/integrations`}
>
Organisation Integrations
</NavSubLink>
)}
{AccountStore.isAdmin() &&
Utils.getFlagsmithHasFeature('organisation_integrations') && (
<NavSubLink
icon={<Icon name='setting' width={24} />}
id='org-settings-link'
data-test='org-settings-link'
to={`/organisation/${AccountStore.getOrganisation().id}/settings`}
icon={<Icon name='layers' />}
id='integrations-link'
to={`/organisation/${
AccountStore.getOrganisation().id
}/integrations`}
>
Organisation Settings
Organisation Integrations
</NavSubLink>
</>
)}
{AccountStore.isAdmin() && (
<NavSubLink
icon={<Icon name='setting' width={24} />}
id='org-settings-link'
data-test='org-settings-link'
to={`/organisation/${AccountStore.getOrganisation().id}/settings`}
>
Organisation Settings
</NavSubLink>
)}
</OverflowNav>
)
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