Skip to content

Replace ActionBar overflow calculations with CSS wrapping approach#7655

Open
iansan5653 wants to merge 25 commits intomainfrom
actionbar-css
Open

Replace ActionBar overflow calculations with CSS wrapping approach#7655
iansan5653 wants to merge 25 commits intomainfrom
actionbar-css

Conversation

@iansan5653
Copy link
Copy Markdown
Contributor

@iansan5653 iansan5653 commented Mar 11, 2026

Takes the CSS flex wrapping approach to detecting overflowing from #7506 and applies it to ActionBar, resulting in a much simpler and more reliable solution that is also SSR-stable:

Screen.Recording.2026-03-11.at.2.37.39.PM.mov

Compare to the current implementation, which flickers on load and doesn't calculate the overflow as accurately, causing items to occasionally get clipped (this is more noticeable on production where it can sometimes calculate the overflow incorrectly; but that's hard to show in Storybook):

Screen.Recording.2026-03-11.at.2.42.41.PM.mov

Changelog

New

Changed

Improves ActionBar overflow calculations and SSR support

Removed

Rollout strategy

  • Patch release
  • Minor release
  • Major release; if selected, include a written rollout or migration plan
  • None; if selected, include a brief description as to why

Testing & Reviewing

Merge checklist

@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Mar 11, 2026

🦋 Changeset detected

Latest commit: d533eb5

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@primer/react Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions github-actions bot added the integration-tests: recommended This change needs to be tested for breaking changes. See https://arc.net/l/quote/tdmpakpm label Mar 11, 2026
@github-actions
Copy link
Copy Markdown
Contributor

⚠️ Action required

👋 Hi, this pull request contains changes to the source code that github/github-ui depends on. If you are GitHub staff, test these changes with github/github-ui using the integration workflow. Check the integration testing docs for step-by-step instructions. Or, apply the integration-tests: skipped manually label to skip these checks.

@iansan5653 iansan5653 marked this pull request as ready for review March 11, 2026 18:45
@iansan5653 iansan5653 requested a review from a team as a code owner March 11, 2026 18:45
@github-actions github-actions bot requested a deployment to storybook-preview-7655 March 11, 2026 18:49 Abandoned
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR refactors ActionBar overflow handling away from JS width calculations and towards a CSS wrapping + overflow-clipping approach (similar to the UnderlineNav work), aiming to reduce flicker and improve SSR stability.

Changes:

  • Replaces ResizeObserver/width math with CSS wrapping + scroll-driven overflow detection.
  • Introduces a per-item overflow registration mechanism using useSyncExternalStore + IntersectionObserver.
  • Adds a changeset for a minor release of @primer/react.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 6 comments.

File Description
packages/react/src/ActionBar/ActionBar.tsx Reworks overflow detection and overflow menu population to use descendant registry updates driven by wrapping/overflow state.
packages/react/src/ActionBar/ActionBar.module.css Adds wrapping container + scroll-timeline based detection to control overflow button visibility.
.changeset/many-suns-promise.md Declares the change as a minor release.
Comments suppressed due to low confidence (1)

packages/react/src/ActionBar/ActionBar.tsx:433

  • ref is cast to RefObject from forwardedRef, but forwardedRef can be a callback ref at runtime. useActionBarItem reads ref.current, which will throw if a callback ref is passed. Consider using an internal useRef plus useRefObjectAsForwardedRef/useMergedRefs so you always have a real object ref for measurements and can still forward refs safely.
export const ActionBarGroup = forwardRef(({children}: React.PropsWithChildren, forwardedRef) => {
  const backupRef = useRef<HTMLDivElement>(null)
  const ref = (forwardedRef ?? backupRef) as RefObject<HTMLDivElement>
  const {id, isOverflowing} = useActionBarItem(
    ref,
    useMemo((): ChildProps => ({type: 'group'}), []),

Comment on lines 449 to 452
) => {
const backupRef = useRef<HTMLButtonElement>(null)
const ref = (forwardedRef ?? backupRef) as RefObject<HTMLButtonElement>
const {isVisibleChild} = React.useContext(ActionBarContext)

Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

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

ref is cast to RefObject from forwardedRef, but forwardedRef can be a callback ref at runtime. Since useActionBarItem and ActionMenu anchorRef rely on ref.current, passing a callback ref here can break at runtime. Consider using an internal object ref and wiring forwardedRef via useRefObjectAsForwardedRef/useMergedRefs.

This issue also appears on line 428 of the same file.

See below for a potential fix:

export const ActionBarGroup = forwardRef<HTMLDivElement, React.PropsWithChildren>(
  ({children}, forwardedRef) => {
    const ref = useRef<HTMLDivElement>(null)
    useRefObjectAsForwardedRef(forwardedRef, ref)
    const {id, isOverflowing} = useActionBarItem(
      ref,
      useMemo((): ChildProps => ({type: 'group'}), []),
    )

    return (
      <ActionBarGroupContext.Provider value={{groupId: id}}>
        <div className={styles.Group} ref={ref} data-overflowing={isOverflowing} aria-hidden={isOverflowing}>
          {children}
        </div>
      </ActionBarGroupContext.Provider>
    )
  },
)

export const ActionBarMenu = forwardRef<HTMLButtonElement, ActionBarMenuProps>(
  (
    {'aria-label': ariaLabel, icon, overflowIcon, items, returnFocusRef, ...props},
    forwardedRef,
  ) => {
    const ref = useRef<HTMLButtonElement>(null)
    useRefObjectAsForwardedRef(forwardedRef, ref)

Copilot uses AI. Check for mistakes.
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.

I'm hoping to ship #7638 first and then use useCombinedRefs from there

@github-actions github-actions bot requested a deployment to storybook-preview-7655 March 11, 2026 18:57 Abandoned
@github-actions github-actions bot temporarily deployed to storybook-preview-7655 March 11, 2026 19:08 Inactive
Copy link
Copy Markdown
Member

@francinelucca francinelucca left a comment

Choose a reason for hiding this comment

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

something's off here 👀

Screen.Recording.2026-03-11.at.3.14.32.PM.mov

@iansan5653
Copy link
Copy Markdown
Contributor Author

something's off here 👀

Should be resolved now by setting visibility:hidden on those items which will remove them from the tab order. Thanks for catching that!

@github-actions github-actions bot requested a deployment to storybook-preview-7655 March 12, 2026 19:27 Abandoned
@github-actions github-actions bot temporarily deployed to storybook-preview-7655 March 12, 2026 19:36 Inactive
@iansan5653 iansan5653 requested a review from francinelucca April 2, 2026 14:59
@github-actions github-actions bot requested a deployment to storybook-preview-7655 April 2, 2026 15:01 Abandoned
@primer
Copy link
Copy Markdown
Contributor

primer bot commented Apr 2, 2026

🤖 Lint and formatting issues have been automatically fixed and committed to this PR.

@github-actions github-actions bot requested a deployment to storybook-preview-7655 April 2, 2026 15:10 Abandoned
@github-actions github-actions bot temporarily deployed to storybook-preview-7655 April 2, 2026 15:21 Inactive
Copy link
Copy Markdown
Member

@francinelucca francinelucca left a comment

Choose a reason for hiding this comment

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

Getting closer, found a few issues:

  1. it looks like the KeyBindingHint story isn't working properly:
Screen.Recording.2026-04-02.at.10.28.33.PM.mov
  1. Something weird is going on with resizing the screen in this story. I can't get back to one of the ActionBars via keyboard:
Screen.Recording.2026-04-02.at.10.39.26.PM.mov
  1. Focus state looks clipped in small ActionBar story
Screen.Recording.2026-04-02.at.10.43.37.PM.mov
  1. These are disappearing
Screen.Recording.2026-04-02.at.10.45.17.PM.mov
  1. When this story is wrapped all the way I can't get to it via keyboard
Screen.Recording.2026-04-02.at.10.49.03.PM.mov

ActionBarItemsRegistry.useRegisterDescendant(isOverflowing ? registryProps : null)

return width
return {isOverflowing, dataOverflowingAttr: isOverflowing ? '' : undefined}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

does this actually need to return the dataOverflowingAttr? couldn't we just look at the value of isOverflowing to set that on consumer side? wondering why both are needed here 🤔

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.

Yeah it totally could and that was how I originally had it. But we'd have to do it for every different kind of item, so it seemed to make sense to just pull the common logic out to here and ensure we are consistent. This is an internal (not consumer facing) hook, so I wasn't too worried about the API being a little redundant

}
return breakpoint
}
const ActionBarItemsRegistry = createDescendantRegistry<ChildProps | null>()
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

does this need to take in the ChildProps? if I'm reading correctly the registry is only read on line 240 to check if there's a non-null value? wondering if this can be simplified if so 🤔

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.

We do need the props because we also use it in L238-240 where we build the list of items for the overflow menu. The props tells us what label to render, what to do on click, etc

@iansan5653
Copy link
Copy Markdown
Contributor Author

Thanks for the thorough review! Looks like tooltips in general aren't working on toolbar items 🤔. I'll dig in to that; maybe they are getting clipped or hidden.

4. These are disappearing

For this one, it doesn't seem like there's really a well-defined preferred behavior here? These buttons aren't ActionBar buttons, so we can't handle them by putting them into the overflow menu. On main, the buttons just overflow off to the right, which feels worse IMO because it pushes the overflow menu out of view.

I think what we should do is just implement an ActionBar.Button component and use that instead, but I didn't want to do that in this PR. Can we be OK with them disappearing for now, knowing that we'll address it with a better solution in the future?

@francinelucca
Copy link
Copy Markdown
Member

On main, the buttons just overflow off to the right, which feels worse IMO because it pushes the overflow menu out of view.

You can still scroll to them vs in this implementation they're completely gone so idk about worse.

I think what we should do is just implement an ActionBar.Button component and use that instead, but I didn't want to do that in this PR. Can we be OK with them disappearing for now, knowing that we'll address it with a better solution in the future?

I agree this seems like an unsupported case. I almost want to remove that example entirely until this is officially supported by ActionBar. CC @TylerJDev I see you introduced the story in #5476, do you remember if there was a reason/use-case for this? want to make sure we don't break anything if we move forward

@iansan5653
Copy link
Copy Markdown
Contributor Author

iansan5653 commented Apr 3, 2026

I almost want to remove that example entirely until this is officially supported by ActionBar.

That also makes sense to me 👍

FWIW it wouldn't be difficult at all to add support for text buttons, especially with the new approach. I could plan to do it in a followup PR.

@iansan5653
Copy link
Copy Markdown
Contributor Author

iansan5653 commented Apr 3, 2026

Okay, turns out the tooltip issue repros in main so it's not a problem with this PR. It seems to have something to do with ref handling and is resolved in another PR I'm working on, #7644.

Still working on the other issues:

2. Something weird is going on with resizing the screen in this story. I can't get back to one of the ActionBars via keyboard:

Could you please explain in more detail what the issue is here? I can't figure out what the bug is from your screencap.

3. Focus state looks clipped in small ActionBar story

Looks like I need to fix the overflow button size when the size="small". Update: resolved ✔️

5. When this story is wrapped all the way I can't get to it via keyboard

Can repro this one; looking in to it. Update: resolved ✔️

@TylerJDev
Copy link
Copy Markdown
Member

I agree this seems like an unsupported case. I almost want to remove that example entirely until this is officially supported by ActionBar. CC @TylerJDev I see you introduced the story in #5476, do you remember if there was a reason/use-case for this? want to make sure we don't break anything if we move forward

I can't remember the context, so I think it's okay to remove! I don't think there's any real world cases that use ActionBar like this.

I did notice one issue in github-ui the staffbar component renders differently in https://github.com/github/github-ui/pull/17750 compared to production storybook when you resize it horizontally (< 500px width). It looks like the items are offscreen 🤔

@iansan5653
Copy link
Copy Markdown
Contributor Author

iansan5653 commented Apr 3, 2026

I think it's okay to remove!

Removed!

I did notice one issue in github-ui the staffbar component renders differently in github/github-ui#17750 compared to production storybook when you resize it horizontally (< 500px width). It looks like the items are offscreen 🤔

Yep, that's the same issue. It's (ab)using ActionBar by rendering labelled items instead of regular action bar items. I'm not too worried about that though because I also maintain that UI and it's very experimental right now

@primer-integration
Copy link
Copy Markdown

👋 Hi from github/github-ui! Your integration PR is ready: https://github.com/github/github-ui/pull/17853

@primer-integration
Copy link
Copy Markdown

Integration test results from github/github-ui:

Passed  CI   Passed
Passed  VRT   Passed
Passed  Projects   Passed

All checks passed!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Canary Release Apply this label when you want CI to create a canary release of the current PR integration-tests: recommended This change needs to be tested for breaking changes. See https://arc.net/l/quote/tdmpakpm

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Avoid flickering when calculating ActionBar overflow

4 participants