Skip to content
Open
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/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
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