diff --git a/.changeset/actionbar-flicker-fix.md b/.changeset/actionbar-flicker-fix.md new file mode 100644 index 00000000000..4219365d26b --- /dev/null +++ b/.changeset/actionbar-flicker-fix.md @@ -0,0 +1,5 @@ +--- +'@primer/react': patch +--- + +ActionBar: Fixed flickering on initial render by applying visibility:hidden during initial width calculations. This prevents items from briefly appearing and then disappearing on slower devices. diff --git a/packages/react/src/ActionBar/ActionBar.test.tsx b/packages/react/src/ActionBar/ActionBar.test.tsx index f176d5e7add..b748eaf8f92 100644 --- a/packages/react/src/ActionBar/ActionBar.test.tsx +++ b/packages/react/src/ActionBar/ActionBar.test.tsx @@ -7,6 +7,22 @@ import {BoldIcon, ItalicIcon, CodeIcon} from '@primer/octicons-react' import {implementsClassName} from '../utils/testing' import classes from './ActionBar.module.css' +// Mock ResizeObserver to trigger callback immediately with a positive width +// This ensures isInitialRender becomes false and the toolbar is visible +const mockUnobserve = vi.fn() +const mockDisconnect = vi.fn() + +globalThis.ResizeObserver = vi.fn().mockImplementation(function (callback) { + return { + observe: vi.fn().mockImplementation(() => { + // Trigger the callback synchronously when observe is called + callback([{contentRect: {width: 500, height: 40}} as ResizeObserverEntry]) + }), + unobserve: mockUnobserve, + disconnect: mockDisconnect, + } +}) + describe('ActionBar', () => { implementsClassName(ActionBar, classes.Nav) afterEach(() => { diff --git a/packages/react/src/ActionBar/ActionBar.tsx b/packages/react/src/ActionBar/ActionBar.tsx index 2ba3b086b9c..a7dcc459c75 100644 --- a/packages/react/src/ActionBar/ActionBar.tsx +++ b/packages/react/src/ActionBar/ActionBar.tsx @@ -303,8 +303,10 @@ export const ActionBar: React.FC> = prop const [childRegistry, setChildRegistry] = ActionBarItemsRegistry.useRegistryState() const [menuItemIds, setMenuItemIds] = useState>(() => new Set()) + const [isInitialRender, setIsInitialRender] = useState(true) const navRef = useRef(null) + // measure gap after first render & whenever gap scale changes useIsomorphicLayoutEffect(() => { if (!listRef.current) return @@ -322,6 +324,7 @@ export const ActionBar: React.FC> = prop if (navWidth > 0) { const newMenuItemIds = getMenuItems(navWidth, moreMenuWidth, childRegistry, hasActiveMenu, computedGap) if (newMenuItemIds) setMenuItemIds(newMenuItemIds) + setIsInitialRender(false) } }, navRef as RefObject) @@ -361,6 +364,7 @@ export const ActionBar: React.FC> = prop aria-label={ariaLabel} aria-labelledby={ariaLabelledBy} data-gap={gap} + style={isInitialRender ? {visibility: 'hidden'} : undefined} > {children} {menuItemIds.size > 0 && (