Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
f75cb4d
Replace `ActionBar` overflow calculations with CSS wrapping approach
iansan5653 Mar 11, 2026
dda90da
Add CSS-based menu visibility
iansan5653 Mar 11, 2026
8f1f205
Add changeset
iansan5653 Mar 11, 2026
83b23e4
Merge branch 'main' into actionbar-css
francinelucca Mar 11, 2026
c0060f3
Set visibility:hidden on overflowing items
iansan5653 Mar 12, 2026
88c6e77
Merge branch 'actionbar-css' of https://github.com/primer/react into …
iansan5653 Mar 12, 2026
ef6c5ec
Add comment re overflow detection
iansan5653 Mar 12, 2026
f3ff80f
Change to use attribute presence instead of str value
iansan5653 Mar 12, 2026
fe98650
Fix items being hidden due to incorrect CSS selector
iansan5653 Mar 13, 2026
46269af
Fix last group overflowing
iansan5653 Mar 13, 2026
6c839cd
Don't show menu button if no valid items registered
iansan5653 Mar 13, 2026
357276a
Format
iansan5653 Mar 13, 2026
1dfff5b
Fix snapshot test
iansan5653 Mar 13, 2026
9b6aeca
Update another test
iansan5653 Mar 13, 2026
7127891
Fix focus zone configuration
iansan5653 Apr 2, 2026
23e7d5f
Use CSS instead of filtering
iansan5653 Apr 2, 2026
81446b2
Merge branch 'main' into actionbar-css
iansan5653 Apr 2, 2026
4c3ff6b
chore: auto-fix lint and formatting issues
iansan5653 Apr 2, 2026
3206cee
Fix selecting overflow button when no items are overflowing
iansan5653 Apr 2, 2026
9b350c1
Merge branch 'actionbar-css' of https://github.com/primer/react into …
iansan5653 Apr 2, 2026
a760177
Remove ocnsole log
iansan5653 Apr 2, 2026
88b089b
Actually fix the issue
iansan5653 Apr 2, 2026
5bc40ac
Fix overflow menu anchor size
iansan5653 Apr 3, 2026
c486d19
Fix focus issue
iansan5653 Apr 3, 2026
d533eb5
Remove text labels story
iansan5653 Apr 3, 2026
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/many-suns-promise.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@primer/react": minor
---

Replace `ActionBar` overflow calculations with CSS wrapping approach to improve performance and stability
4 changes: 2 additions & 2 deletions e2e/components/drafts/ActionBar.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,12 @@ test.describe('ActionBar', () => {
},
})
const toolbarButtonSelector = `button[data-component="IconButton"]`
await expect(page.locator(toolbarButtonSelector)).toHaveCount(10)
await expect(page.locator(toolbarButtonSelector).filter({visible: true})).toHaveCount(10)
await page.setViewportSize({width: viewports['primer.breakpoint.xs'], height: 768})
await page.getByLabel('Task List').waitFor({
state: 'hidden',
})
await expect(page.locator(toolbarButtonSelector)).toHaveCount(8)
await expect(page.locator(toolbarButtonSelector).filter({visible: true})).toHaveCount(8)
const moreButtonSelector = page.getByLabel('More Comment box toolbar items')
await moreButtonSelector.click()
await expect(page.locator('ul[role="menu"] [role="menuitem"]')).toHaveCount(3)
Expand Down
8 changes: 0 additions & 8 deletions packages/react/src/ActionBar/ActionBar.examples.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,6 @@ export const WithGroups = () => (
</ActionBar>
)

export const TextLabels = () => (
<ActionBar aria-label="Toolbar">
<Button>Edit</Button>
<Button>Duplicate</Button>
<Button>Export to CSV</Button>
</ActionBar>
)

export const SmallActionBar = () => (
<ActionBar size="small" aria-label="Toolbar">
<ActionBar.IconButton icon={BoldIcon} aria-label="Bold"></ActionBar.IconButton>
Expand Down
56 changes: 54 additions & 2 deletions packages/react/src/ActionBar/ActionBar.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,34 @@
/* wonder why this is here */
/* stylelint-disable-next-line primer/spacing */
margin-bottom: -1px;
white-space: nowrap;
list-style: none;
align-items: center;
align-items: flex-start;
gap: var(--actionbar-gap, var(--stack-gap-condensed));
overflow: hidden;
/* Explicit height is required to clip wrapped items */
height: var(--actionbar-height, var(--control-small-size));

/* Only apply scroll animation before overflow is calculated (for SSR/initial render) */
&:not([data-has-overflow]) {
/* Scroll-based animations have no effect unless the container is scrollable (has overflow, even with overflow:hidden)
so we can use them to detect overflow. It would be cleaner to use scroll-state container queries for this, but
browser support for scroll-driven animations is slightly better. */
animation: detect-overflow linear;
animation-timeline: scroll(self block);
}

/* After initial render, JS is used to control visibility which provides progressive enhancement for unsupported browsers */
&[data-has-overflow='true'] {
--morebutton-display: block;
}

&:where([data-size='medium']) {
--actionbar-height: var(--control-medium-size);
}

&:where([data-size='large']) {
--actionbar-height: var(--control-large-size);
}

/* Gap scale (mirrors Stack) */
&:where([data-gap='none']) {
Expand All @@ -23,6 +47,20 @@
&:where([data-gap='condensed']) {
--actionbar-gap: var(--stack-gap-condensed);
}

& [data-overflowing] {
/* Hide overflowing items. Even though they are clipped by `overflow: hidden`, setting `visibility: hidden` ensures
they can't accidentally be shown and also hides them from screen readers / keyboard nav. `!important` prevents
consumers from unintentionally overriding this and breaking accessibility. */
visibility: hidden !important;
}
}

@keyframes detect-overflow {
0%,
100% {
--morebutton-display: block;
}
}

.Nav {
Expand All @@ -44,10 +82,24 @@
content: '';
/* stylelint-disable-next-line primer/colors */
background: var(--borderColor-muted);
/* stylelint-disable-next-line primer/spacing */
margin-top: calc((var(--actionbar-height) - var(--base-size-20)) / 2);
}
}

.Group {
display: flex;
gap: inherit;
}

.OverflowContainer {
display: flex;
flex-wrap: wrap;
gap: inherit;
justify-content: flex-end;
overflow: hidden;
}

.MoreButton {
display: var(--morebutton-display, none);
}
5 changes: 3 additions & 2 deletions packages/react/src/ActionBar/ActionBar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -228,13 +228,14 @@ describe('ActionBar Registry System', () => {
render(
<div style={{width: 0, overflow: 'hidden'}}>
<ActionBar aria-label="Zero width">
<ActionBar.IconButton icon={BoldIcon} aria-label="Zero width button" />
<ActionBar.IconButton icon={BoldIcon} aria-label="Zero width button" data-testid="zero-width-button" />
</ActionBar>
</div>,
)

// Component should still render even with zero width
expect(screen.getByRole('button', {name: 'Zero width button'})).toBeInTheDocument()
// Button is unlabeled because the label is hidden, so we select by test id instead
expect(screen.getByTestId('zero-width-button')).toBeInTheDocument()
})

it('should clean up registry on unmount', async () => {
Expand Down
Loading
Loading