Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
5 changes: 5 additions & 0 deletions .changeset/proud-candles-mix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@primer/react": patch
---

`UnderlineNav`: Fix icon flickering on some screen sizes before initial render/hydration
35 changes: 27 additions & 8 deletions packages/react/src/UnderlineNav/UnderlineNav.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,50 @@
/* Progressive enhancement: Detect overflow using scroll-based animations.
The idiomatic way would be a scroll-state container query but browser support
is slightly better for animations. */
animation: detect-overflow linear;
animation: detect-overflow;
animation-timeline: scroll(self block);

--UnderlineNav_moreButton-visibility: hidden;
--UnderlineNav_icons-display: inline;

&[data-hide-icons='true'] {
--UnderlineNav_icons-display: none;
}
--UnderlineNav_hide-icons-play-state: paused;

&[data-has-overflow='true'] {
--UnderlineNav_moreButton-visibility: visible;
}

&[data-hide-icons='true'] [data-component='icon'] {
display: none;
}
}

@keyframes detect-overflow {
0%,
100% {
--UnderlineNav_moreButton-visibility: visible;
--UnderlineNav_icons-display: none;
--UnderlineNav_hide-icons-play-state: running;
}
}

.ItemsList [data-component='icon'] {
display: var(--UnderlineNav_icons-display);
/* Unlike the more button, the icons are removed from the layout when hidden. This can cause the container to no
longer overflow, which would cause a flickering loop if we just drove the visibility directly from the scroll state.
So instead we drive an animation that can only play forwards so the icons stay hidden for the life of the page, even
if the container is no longer overflowing. We can't just make the scroll-driven animation itself play forwards,
because scroll-driven animations are conditionally applied and revert when not scrollable. */
animation-name: hide-icons;
animation-fill-mode: forwards;
animation-play-state: var(--UnderlineNav_hide-icons-play-state);
animation-duration: 0.1ms; /* must be greater than 0 */

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interestingly, this can't be arbitrarily small; 0.0001ms is treated as 0 in Chrome. 0.1ms seems to work very consistently though.

I did try making the duration dynamic, instead of the play state, and setting the duration to 0 to immediately resolve the animation when I want to toggle it. That didn't work though; the style didn't 'stick' if the duration was increased again.

}

@keyframes hide-icons {
0% {
display: inline;
}

0.1%,
100% {
display: none;
}
}
Comment thread
iansan5653 marked this conversation as resolved.

.MoreButtonContainer {
Expand Down
5 changes: 2 additions & 3 deletions packages/react/src/UnderlineNav/UnderlineNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,8 @@ export const UnderlineNav = forwardRef(
ref={navRef}
data-variant={variant}
data-overflow-mode="wrap"
// Force icons to stay hidden, avoiding flickering as icons create/remove overflow
// Ensure icons are hidden and button is shown on browsers that don't support scroll-driven animations
data-hide-icons={hasEverOverflowed ? 'true' : undefined}
// Ensure button is shown (after initial render) on browsers that don't support scroll-driven animations
data-has-overflow={isOverflowing ? 'true' : undefined}
>
<UnderlineItemList ref={listRef} role="list" className={classes.ItemsList}>
Expand All @@ -115,7 +114,7 @@ export const UnderlineNav = forwardRef(
variant="invisible"
data-component="overflow-menu-button"
data-current={overflowingCurrentItem ? 'true' : undefined}
aria-label={overflowingCurrentItem ? `More items, including current item` : undefined}
aria-label={overflowingCurrentItem ? 'More items, including current item' : undefined}
>
<span>
More<VisuallyHidden as="span"> items</VisuallyHidden>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,14 @@
padding-inline: unset;
}

&[data-hide-icons='true'] [data-component='icon'] {
display: none;
}

&[data-overflow-mode='wrap'] {
/* Wrap items onto hidden next lines */
overflow: hidden;
max-height: var(--control-xlarge-size);

.UnderlineItemList {
flex-wrap: wrap;
overflow: hidden;
flex: 1;
Comment on lines -32 to +28

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

overflow: hidden had no effect here because there's no explicit height set on this container. And it's unnecessary since the parent already has that applied.

flex: 1 fixes a separate bug where sometimes the button wouldn't float all the way to the right. It makes the tab containers take up more space to push the overflow button further out. This prevents it from looking like it's floating in space.

}
}
}
Expand Down
Loading