Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
7b8e794
Add overflow: hidden logic
TylerJDev Feb 6, 2026
a0e4f42
Add changeset
TylerJDev Feb 6, 2026
604c035
Run format
TylerJDev Feb 6, 2026
7fc6739
Update packages/react/src/internal/components/UnderlineTabbedInterfac…
TylerJDev Feb 6, 2026
6921a86
Update packages/react/src/internal/components/UnderlineTabbedInterfac…
TylerJDev Feb 6, 2026
1bc03b6
Update packages/react/src/UnderlineNav/UnderlineNav.tsx
TylerJDev Feb 6, 2026
cc8b23b
WIP: wrap items out of the way during initial render, using scroll-st…
iansan5653 Feb 6, 2026
be8b640
Migrate as much logic as possible to CSS, allowing elements to regist…
iansan5653 Feb 6, 2026
f5b4a72
Add comments about registry width
iansan5653 Feb 6, 2026
ff30931
Remove opacity from item
iansan5653 Feb 6, 2026
dfda985
Disable menu item anchor when empty and hidden
iansan5653 Feb 6, 2026
ab2806c
Remove unecessary memo
iansan5653 Feb 6, 2026
588223e
Add todo comment
iansan5653 Feb 6, 2026
68f2ef1
Add `overflow: hidden` to parent list
iansan5653 Feb 6, 2026
184ea86
Disable stylelint error for scroll-state rule
iansan5653 Feb 9, 2026
591b5ec
Fix failing unit tests
iansan5653 Feb 9, 2026
b65d397
Truncate last menu item
iansan5653 Feb 9, 2026
86d6897
perf(Announce): skip getComputedStyle when there is no text content t…
hectahertz Feb 18, 2026
0c2358a
perf(ActionList): add content-visibility: auto to reduce style recalc…
hectahertz Feb 18, 2026
f25bb1c
chore(deps-dev): bump ajv from 8.16.0 to 8.18.0 (#7561)
dependabot[bot] Feb 18, 2026
551ec63
perf(Button): fix CounterLabel remount, remove DEV hook, enable React…
hectahertz Feb 18, 2026
e0773f5
Replace overflow menu with `ActionMenu`
iansan5653 Feb 18, 2026
8824b2d
Clean up menu-only edge case (unreachable with truncation)
iansan5653 Feb 18, 2026
399300c
Migrate styles to CSS
iansan5653 Feb 18, 2026
500e706
Merge branch 'main' of https://github.com/primer/react into underline…
iansan5653 Feb 18, 2026
35735d2
Improve CSS comments
iansan5653 Feb 18, 2026
3deb5f9
chore: auto-fix lint and formatting issues
iansan5653 Feb 18, 2026
de7ff00
Replace `ResizeObserver` at container level with `IntersectionObserve…
iansan5653 Feb 18, 2026
e01b457
Merge branch 'underline-nav-full-css-spike' of https://github.com/pri…
iansan5653 Feb 18, 2026
690943a
Fix overflow: hidden
iansan5653 Feb 18, 2026
b06222b
Fix underline tabbed panels and swap scroll-state for animation
iansan5653 Feb 20, 2026
ead0546
Update assertion per updated label text
iansan5653 Feb 20, 2026
82d821e
Add margins to stop overflow clipping underline boundary
iansan5653 Feb 23, 2026
cc1e347
Fix registration ordering
iansan5653 Feb 23, 2026
9d3ba17
Simplify underline positioning per TODO comment
iansan5653 Feb 23, 2026
ec9462f
Update menu item role
iansan5653 Feb 23, 2026
b34a523
Update snapshots
iansan5653 Feb 23, 2026
c1dd8f1
Simplify calculation for underline positioning (per TODO)
iansan5653 Feb 23, 2026
d98c04b
Remove unecessary nbsp
iansan5653 Feb 23, 2026
ec2c47c
Remove spec for preserving current item in top-level menu
iansan5653 Feb 23, 2026
df15a1e
Add decoration to current item in overflow menu
iansan5653 Feb 23, 2026
eef80ba
chore: auto-fix lint and formatting issues
iansan5653 Feb 23, 2026
0f8782b
Merge branch 'descendant-registry-pattern' of https://github.com/prim…
iansan5653 Feb 25, 2026
c7abf30
Update to use descendant registry pattern
iansan5653 Feb 25, 2026
eacda5b
Merge branch 'descendant-registry-pattern' into underline-nav-full-cs…
iansan5653 Feb 25, 2026
4a90231
Revert focused test
iansan5653 Feb 25, 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/announce-skip-empty-reflow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@primer/react': patch
---

perf(Announce): skip getComputedStyle when there is no text content to announce
5 changes: 5 additions & 0 deletions .changeset/button-optimize-render.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@primer/react': patch
---

perf(Button): fix CounterLabel remount and remove conditional DEV hook
5 changes: 5 additions & 0 deletions .changeset/content-visibility-perf.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@primer/react': patch
---

Add `content-visibility: auto` to ActionList items to improve rendering performance for large lists by allowing the browser to skip layout and paint for off-screen items.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
66 changes: 9 additions & 57 deletions e2e/components/UnderlineNav.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,19 +199,19 @@ test.describe('UnderlineNav', () => {
})

// Default state
// expect(await page.screenshot()).toMatchSnapshot()
expect(await page.screenshot()).toMatchSnapshot()

await page.setViewportSize({width: viewports['primer.breakpoint.sm'], height: 768})
await page.locator('button', {hasText: 'More Repository Items'}).waitFor()
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've updated the label of the overflow button to "More items", aligning with this issue for ActionBar: #7437

await page.locator('button', {hasText: 'More items'}).waitFor()

// Resize
// expect(await page.screenshot()).toMatchSnapshot()
expect(await page.screenshot()).toMatchSnapshot()

await page.getByRole('button', {name: 'More Repository Items'}).click()
// expect(await page.screenshot()).toMatchSnapshot()
await page.getByRole('button', {name: 'More items'}).click()
expect(await page.screenshot()).toMatchSnapshot()

await page.getByRole('link', {name: 'Settings (10)'}).click()
// expect(await page.screenshot()).toMatchSnapshot()
await page.getByRole('menuitem', {name: 'Settings (10)'}).click()
expect(await page.screenshot()).toMatchSnapshot()
})

test('Hide icons when there is not enough space to display all list items @vrt', async ({page}) => {
Expand All @@ -223,61 +223,13 @@ test.describe('UnderlineNav', () => {
})

// Default State
// expect(await page.screenshot()).toMatchSnapshot()
expect(await page.screenshot()).toMatchSnapshot()

// Resize
await page.setViewportSize({width: viewports['primer.breakpoint.md'], height: 768})

// Icons should be hidden
// expect(await page.screenshot()).toMatchSnapshot()
})

test('Keep selected item visible @vrt', async ({page}) => {
await visit(page, {
id: 'components-underlinenav-features--overflow-template',
globals: {
colorScheme: theme,
},
})
await page.setViewportSize({width: viewports['primer.breakpoint.sm'], height: 768})

await page.locator('button', {hasText: 'More Repository Items'}).waitFor()
await page.getByRole('button', {name: 'More Repository Items'}).click()
await page.getByRole('link', {name: 'Settings (10)'}).click()

// State after selecting the second last item
// expect(await page.screenshot()).toMatchSnapshot()

// Resize
await page.setViewportSize({
width: 1100,
height: 480,
})
await page.locator('button', {hasText: 'More Repository Items'}).waitFor({
state: 'hidden',
})

// Current state
// expect(await page.screenshot()).toMatchSnapshot()

// Resize
await page.setViewportSize({
width: 800,
height: 480,
})
await page.locator('button', {hasText: 'More Repository Items'}).waitFor()

// Current state
// expect(await page.screenshot()).toMatchSnapshot()

// Resize
await page.setViewportSize({
width: 600,
height: 480,
})
await page.locator('button', {hasText: 'More Repository Items'}).waitFor()
// Current state
// expect(await page.screenshot()).toMatchSnapshot()
expect(await page.screenshot()).toMatchSnapshot()
})
})
}
Expand Down
39 changes: 12 additions & 27 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@
"@types/react-is": "18.3.1",
"@vitejs/plugin-react": "^4.3.3",
"afterframe": "^1.0.2",
"ajv": "8.16.0",
"ajv": "8.18.0",
"axe-core": "4.9.1",
"babel-core": "7.0.0-bridge.0",
"babel-plugin-add-react-displayname": "0.0.5",
Expand Down
1 change: 0 additions & 1 deletion packages/react/script/react-compiler.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ const unsupported = new Set(
[
'src/ActionMenu/**/*.tsx',
'src/AvatarStack/**/*.tsx',
'src/Button/**/*.tsx',
'src/ConfirmationDialog/**/*.tsx',
'src/Pagehead/**/*.tsx',
'src/Pagination/**/*.tsx',
Expand Down
39 changes: 16 additions & 23 deletions packages/react/src/Button/ButtonBase.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,24 +60,19 @@ const ButtonBase = forwardRef(({children, as: Component = 'button', ...props}, f
const ariaDescribedByIds = loading ? [loadingAnnouncementID, ariaDescribedBy] : [ariaDescribedBy]

if (__DEV__) {
/**
* The Linter yells because it thinks this conditionally calls an effect,
* but since this is a compile-time flag and not a runtime conditional
* this is safe, and ensures the entire effect is kept out of prod builds
* shaving precious bytes from the output, and avoiding mounting a noop effect
*/
// eslint-disable-next-line react-hooks/rules-of-hooks
React.useEffect(() => {
if (
innerRef.current &&
!(innerRef.current instanceof HTMLButtonElement) &&
!((innerRef.current as unknown) instanceof HTMLAnchorElement) &&
!((innerRef.current as HTMLElement).tagName === 'SUMMARY')
) {
// eslint-disable-next-line no-console
console.warn('This component should be an instanceof a semantic button or anchor')
}
}, [innerRef])
// Validate that the element is a semantic button/anchor.
// This runs during render (not in an effect) to avoid a conditional hook call
// that prevents React Compiler from optimizing this component.
const el = innerRef.current
if (
el &&
!(el instanceof HTMLButtonElement) &&
!((el as unknown) instanceof HTMLAnchorElement) &&
!((el as HTMLElement).tagName === 'SUMMARY')
) {
// eslint-disable-next-line no-console
console.warn('This component should be an instanceof a semantic button or anchor')
}
}
return (
<ConditionalWrapper
Expand Down Expand Up @@ -154,11 +149,9 @@ const ButtonBase = forwardRef(({children, as: Component = 'button', ...props}, f
*/
count !== undefined && !TrailingVisual
? renderModuleVisual(
() => (
<CounterLabel className={classes.CounterLabel} data-component="ButtonCounter">
{count}
</CounterLabel>
),
<CounterLabel className={classes.CounterLabel} data-component="ButtonCounter">
{count}
</CounterLabel>,
Boolean(loading) && !LeadingVisual,
'trailingVisual',
true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@
height: 100%;
overflow: auto;
flex-grow: 1;

/* Allow the browser to skip rendering for off-screen items, reducing style recalc and layout costs in long lists.
Exclude items that show the active indicator line, as content-visibility: auto applies paint containment
which clips the absolutely-positioned ::after pseudo-element that renders outside the item bounds. */
& .ActionListItem:not(:focus, [data-is-active-descendant], [data-active], [data-input-focused]) {
content-visibility: auto;
contain-intrinsic-size: auto 32px;
}
}

.ActionList {
Expand Down
7 changes: 3 additions & 4 deletions packages/react/src/TreeView/TreeView.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1440,16 +1440,15 @@ describe('Asynchronous loading', () => {
const {getByRole} = renderWithTheme(<TestTree />)

const doneButton = getByRole('button', {name: 'Load'})
const liveRegion = getLiveRegion()

// Live region should be empty
expect(liveRegion.getMessage('polite')).toBe('')

// Click load button to mimic async loading
await act(async () => {
await user.click(doneButton)
})

// Get live region after the first announcement creates it
const liveRegion = getLiveRegion()

expect(liveRegion.getMessage('polite')).toBe('Parent content loading')

// Click done button to mimic the completion of async loading
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,7 @@ const items: {navigation: string; icon: React.ReactElement; counter?: number | s
export const OverflowTemplate = ({initialSelectedIndex = 1}: {initialSelectedIndex?: number}) => {
const [selectedIndex, setSelectedIndex] = React.useState<number | null>(initialSelectedIndex)
return (
<UnderlineNav
aria-label="Repository"
// @ts-ignore UnderlineNav does not take selectionVariant prop, but we need to pass it to the underlying ActionList so it doesn't show Selections.
selectionVariant={undefined}
>
<UnderlineNav aria-label="Repository">
{items.map((item, index) => (
<UnderlineNav.Item
key={item.navigation}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ SelectAMenuItem.play = async ({canvasElement}: {canvasElement: HTMLElement}) =>

await delay(1000)

const moreBtn = canvas.getByRole('button', {name: 'More Repository items'})
const moreBtn = canvas.getByRole('button', {name: 'More items'})
userEvent.hover(moreBtn)

await delay()
Expand All @@ -131,35 +131,4 @@ SelectAMenuItem.play = async ({canvasElement}: {canvasElement: HTMLElement}) =>
expect(lastListItem).toEqual(menuListItem)
}

const KeepSelectedItemVisible = () => {
return <OverflowTemplate initialSelectedIndex={7} />
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
KeepSelectedItemVisible.play = async ({canvasElement}: {canvasElement: HTMLElement}) => {
const canvas = within(canvasElement)
// await delay(2000)
const selectedItem = canvas.getByRole('link', {name: 'Settings (10)'})
expect(selectedItem).toHaveAttribute('aria-current', 'page')
// change viewport
canvasElement.style.width = '900px'
await delay(1000)
expect(selectedItem).toHaveAttribute('aria-current', 'page')
canvasElement.style.width = '800px'
await delay(1000)
expect(selectedItem).toHaveAttribute('aria-current', 'page')
canvasElement.style.width = '700px'
await delay(1000)
expect(selectedItem).toHaveAttribute('aria-current', 'page')
canvasElement.style.width = '600px'
await delay(1000)
expect(selectedItem).toHaveAttribute('aria-current', 'page')
canvasElement.style.width = '500px'
await delay(1000)
const lastListItem = canvas.getByRole('list').children[2].children[0]
const menuListItem = canvas.getByRole('link', {name: 'Settings (10)'})
// expect Settings be the last element on the list.
expect(lastListItem).toEqual(menuListItem)
}

export {KeyboardNavigation, SelectAMenuItem, KeepSelectedItemVisible}
export {KeyboardNavigation, SelectAMenuItem}
Loading
Loading