Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
a04b7bd
Add BranchName WithTrailingAction story
siddharthkp Jun 3, 2026
aad16e8
Add e2e tests for WithTrailingAction stories
siddharthkp Jun 8, 2026
fd4ede3
test(vrt): update snapshots
siddharthkp Jun 10, 2026
1a98046
Use announce() instead of rendered Announce component
siddharthkp Jun 15, 2026
8d3071f
Add BranchName API proposal
siddharthkp Jun 15, 2026
21de126
Implement BranchName compound API
siddharthkp Jun 16, 2026
658a69b
Fix BranchName LeadingVisual vertical alignment
siddharthkp Jun 16, 2026
1442a29
Merge remote-tracking branch 'origin/main' into siddharthkp/branchnam…
siddharthkp Jun 16, 2026
25eda48
update changeset
siddharthkp Jun 16, 2026
747843c
Consolidate BranchName tests and document compound API in docs.json
siddharthkp Jun 16, 2026
cfe2bb4
Fix unnecessary optional chain lint error in BranchName
siddharthkp Jun 17, 2026
f74c96a
Remove BranchName API proposal doc
siddharthkp Jun 17, 2026
9f253cc
Update BranchName e2e stories to match current story IDs
siddharthkp Jun 17, 2026
06edaec
test(vrt): update snapshots
siddharthkp Jun 17, 2026
9c18812
Add slot markers and simplify BranchName to a single trailing action
siddharthkp Jun 18, 2026
7749254
Make BranchName.LeadingVisual children-only to match other LeadingVis…
siddharthkp Jun 18, 2026
7397e5f
BranchName: forward props/merge className on slots, simplify tooltip …
siddharthkp Jun 18, 2026
a4553a2
manual refactor
siddharthkp Jun 18, 2026
1d876f4
chore: auto-fix lint and formatting issues
siddharthkp Jun 18, 2026
203aa28
refactor(BranchName): narrow polymorphic 'as' to span | a and remove …
siddharthkp Jun 18, 2026
c3804a3
test(BranchName): update types test for narrowed span | a polymorphism
siddharthkp Jun 18, 2026
76f16c7
Merge branch 'main' into siddharthkp/branchname-trailing-action-compo…
joshblack Jun 22, 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/whole-rings-glow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@primer/react": minor
---

BranchName: Add `leadingVisual`, `trailingAction` and `description` prop
Comment on lines +1 to +5
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.

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.

double-checking that the change in spacing here is expected

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.

noticed some of these are displaying tooltips and some aren't, just want to make sure this doesn't become flaky vrt 👀

24 changes: 23 additions & 1 deletion e2e/components/BranchName.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,27 @@ const stories = [
focus: false,
},
{
// Note: the story was renamed to "With Leading Visual" but we keep the
// snapshot title as "With A Branch Icon" so the VRT diff stays usable.
Comment on lines +17 to +18

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.

curious how this works 👀

title: 'With A Branch Icon',
id: 'components-branchname-features--with-branch-icon',
id: 'components-branchname-features--with-leading-visual',
focus: false,
},
{
title: 'Link Without Href',
id: 'components-branchname-features--link-without-href',
focus: true,
},
{
title: 'No Props',
id: 'components-branchname-features--no-props',
focus: false,
},
{
title: 'With Description',
id: 'components-branchname-features--with-description',
focus: true,
},
{
title: 'With Trailing Action',
id: 'components-branchname-features--with-trailing-action',
Expand All @@ -28,6 +45,11 @@ const stories = [
id: 'components-branchname-features--with-trailing-action-menu',
focus: false,
},
{
title: 'Kitchen Sink',
id: 'components-branchname-features--kitchen-sink',
focus: false,
},
] as const

test.describe('BranchName', () => {
Expand Down
94 changes: 92 additions & 2 deletions packages/react/src/BranchName/BranchName.docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,16 @@
"id": "components-branchname--default"
},
{
"id": "components-branchname-features--with-branch-icon"
"id": "components-branchname-features--with-leading-visual"
},
{
"id": "components-branchname-features--with-description"
},
{
"id": "components-branchname-features--with-trailing-action"
},
{
"id": "components-branchname-features--with-trailing-action-menu"
},
{
"id": "components-branchname-features--not-a-link"
Expand All @@ -24,7 +33,88 @@
"name": "as",
"type": "React.ElementType",
"defaultValue": "\"a\""
},
{
"name": "children",
"type": "React.ReactNode",
"required": false,
"description": "The branch name, optionally including BranchName.LeadingVisual and BranchName.TrailingAction subcomponents.",
"defaultValue": ""
},
{
"name": "description",
"type": "string",
"required": false,
"description": "Description tooltip text. Provides additional context for the branch name and is exposed to assistive technology via `aria-describedby`.",
"defaultValue": ""
},
{
"name": "className",
"type": "string",
"required": false,
"description": "Custom className",
"defaultValue": ""
}
],
"subcomponents": []
"subcomponents": [
{
"name": "BranchName.LeadingVisual",
"props": [
{
"name": "children",
"type": "React.ReactNode",
"required": false,
"description": "The leading visual content, typically an octicon. We recommend using `GitBranchIcon` to represent a branch.",
"defaultValue": ""
},
{
"name": "className",
"type": "string",
"required": false,
"description": "A custom class name that is merged with the leading visual's class. Additional `span` props are forwarded to the underlying element.",
"defaultValue": ""
}
Comment on lines +70 to +76
]
},
{
"name": "BranchName.TrailingAction",
"props": [
{
"name": "icon",
"type": "React.ComponentType",
"required": true,
"description": "Provide an octicon. It will be placed in the center of the action button.",
"defaultValue": ""
},
{
"name": "aria-label",
"type": "string",
"required": true,
"description": "Use an aria label to describe the functionality of the button. Please refer to [our guidance on alt text](https://primer.style/guides/accessibility/alternative-text-for-images) for tips on writing good alternative text.",
"defaultValue": ""
},
{
"name": "onClick",
"type": "React.MouseEventHandler<HTMLButtonElement>",
"required": false,
"description": "Handler called when the trailing action button is clicked.",
"defaultValue": ""
},
{
"name": "className",
"type": "string",
"required": false,
"description": "A custom class name that is merged with the action button's class. Additional `button` props are forwarded to the underlying button.",
"defaultValue": ""
},
{
"name": "ref",
"type": "React.RefObject<HTMLButtonElement>",
"required": false,
"description": "A ref forwarded to the underlying action button.",
"defaultValue": ""
}
]
}
]
}
114 changes: 69 additions & 45 deletions packages/react/src/BranchName/BranchName.features.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,22 @@
import type {Meta} from '@storybook/react-vite'
import React, {useState} from 'react'
import React, {useState, useRef} from 'react'
import BranchName from './BranchName'
import {Stack} from '../Stack'
import Octicon from '../Octicon'
import {GitBranchIcon, CopyIcon, CheckIcon, TriangleDownIcon} from '@primer/octicons-react'
import {IconButton} from '../Button'
import {Tooltip} from '../TooltipV2'
import {SelectPanel} from '../SelectPanel'
import type {ItemInput} from '../FilteredActionList'
import {clsx} from 'clsx'
import {announce} from '@primer/live-region-element'

import styles from './BranchName.stories.module.css'

export default {
title: 'Components/BranchName/Features',
component: BranchName,
} as Meta<typeof BranchName>

export const WithBranchIcon = () => (
export const WithLeadingVisual = () => (
<BranchName href="#">
<Stack direction="horizontal" gap="condensed" align="center">
<Octicon icon={GitBranchIcon} />
branch_name
</Stack>
Comment on lines -23 to -26

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.

is the previous way still supported? just making sure this isn't a breaking change

<BranchName.LeadingVisual>
<GitBranchIcon />
</BranchName.LeadingVisual>
branch_name
</BranchName>
)

Expand All @@ -38,6 +31,18 @@ export const LinkWithoutHref = () => (

export const NoProps = () => <BranchName>branch_name_no_props</BranchName>

export const WithDescription = ({
branchName = 'fix/anchored-overlay-outside-top-autogrow',
repo = 'primer/react',
}: {
branchName: string
repo: string
}) => (
<BranchName href={`https://github.com/${repo}/tree/${branchName}`} description={`${repo}:${branchName}`}>
{branchName}
</BranchName>
)

export const WithTrailingAction = ({
branchName = 'fix/anchored-overlay-outside-top-autogrow',
repo = 'primer/react',
Expand All @@ -54,26 +59,15 @@ export const WithTrailingAction = ({
setTimeout(() => setCopied(false), 2000)
}

const tooltipText = copied ? 'Copied!' : 'Copy branch name to clipboard'

return (
<span className={styles.BranchNameWithTrailingAction}>
<Tooltip text={`${repo}:${branchName}`}>
<BranchName href={`https://github.com/${repo}/tree/${branchName}`} className={styles.BranchNameTransparent}>
{branchName}
</BranchName>
</Tooltip>
<Tooltip text={tooltipText} aria-hidden>
<IconButton
icon={copied ? CheckIcon : CopyIcon}
aria-label="Copy branch name to clipboard"
variant="invisible"
size="small"
onClick={handleCopy}
className={clsx(styles.TrailingActionButton, copied && styles.TrailingActionButtonCopied)}
/>
</Tooltip>
</span>
<BranchName href={`https://github.com/${repo}/tree/${branchName}`} description={`${repo}:${branchName}`}>
{branchName}
<BranchName.TrailingAction
icon={copied ? CheckIcon : CopyIcon}
aria-label="Copy branch name to clipboard"
onClick={handleCopy}
/>
</BranchName>
)
}

Expand All @@ -94,6 +88,7 @@ export const WithTrailingActionMenu = ({repo = 'primer/react'}: {repo: string})
const [selected, setSelected] = useState<ItemInput>(branches[0])
const [filter, setFilter] = useState('')
const [open, setOpen] = useState(false)
const anchorRef = useRef<HTMLButtonElement>(null)

const filteredBranches = branches.filter(branch => branch.text?.toLowerCase().includes(filter.toLowerCase()))

Expand All @@ -104,21 +99,19 @@ export const WithTrailingActionMenu = ({repo = 'primer/react'}: {repo: string})
}

return (
<span className={styles.BranchNameWithTrailingAction}>
<BranchName href={`https://github.com/${repo}/tree/${selected.text}`} className={styles.BranchNameTransparent}>
Comment on lines -107 to -108

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.

any dead css as a result of this we can cleanup?

<>
<BranchName href={`https://github.com/${repo}/tree/${selected.text}`}>
{selected.text}
<BranchName.TrailingAction
icon={TriangleDownIcon}
aria-label="Change base branch"
ref={anchorRef}
onClick={() => setOpen(!open)}
/>
</BranchName>
<SelectPanel
renderAnchor={anchorProps => (
<IconButton
icon={TriangleDownIcon}
aria-label="Change base branch"
variant="invisible"
size="small"
className={styles.TrailingActionButton}
{...anchorProps}
/>
)}
renderAnchor={null}
anchorRef={anchorRef}
placeholder="Find a branch..."
open={open}
onOpenChange={setOpen}
Expand All @@ -129,6 +122,37 @@ export const WithTrailingActionMenu = ({repo = 'primer/react'}: {repo: string})
title="Change base branch"
overlayProps={{width: 'medium'}}
/>
</span>
</>
)
}

export const KitchenSink = ({
branchName = 'fix/anchored-overlay-outside-top-autogrow',
repo = 'primer/react',
}: {
branchName: string
repo: string
}) => {
const [copied, setCopied] = React.useState(false)

const handleCopy = () => {
setCopied(true)
void navigator.clipboard.writeText(branchName)
announce('Copied!')
setTimeout(() => setCopied(false), 2000)
}

return (
<BranchName href={`https://github.com/${repo}/tree/${branchName}`} description={`${repo}:${branchName}`}>
<BranchName.LeadingVisual>
<GitBranchIcon />
</BranchName.LeadingVisual>
{branchName}
<BranchName.TrailingAction
icon={copied ? CheckIcon : CopyIcon}
aria-label="Copy branch name to clipboard"
onClick={handleCopy}
/>
</BranchName>
)
}
62 changes: 61 additions & 1 deletion packages/react/src/BranchName/BranchName.module.css
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
.BranchName {
display: inline-block;
display: inline-flex;
align-items: center;
padding: var(--base-size-2) var(--base-size-6);
font-family: var(--fontStack-monospace);
font-size: var(--text-body-size-small);
Expand All @@ -11,4 +12,63 @@
&:is(:not(a)) {
color: var(--fgColor-muted);
}

[data-component='BranchName.LeadingVisual'] {
display: inline-flex;
align-items: center;
margin-inline-end: var(--base-size-4);
}
}

/* Container for BranchName with trailing actions */
.BranchNameWithTrailingAction {
display: inline-flex;
align-items: stretch;

/* Create room for pseudo-border divider */
gap: var(--borderWidth-default);
flex-wrap: nowrap;
/* Moving the background to the container allows stacking the button hover background over it */
background-color: var(--bgColor-accent-muted);
border-radius: var(--borderRadius-medium);

[data-component='BranchName'] {
background-color: transparent;
}

[data-component='BranchName.TrailingAction'] {
display: inline-flex;
align-items: stretch;
}

[data-component='IconButton'] {
/* Make the size auto calculated based on icon size and padding */
height: auto;
width: auto;
padding: var(--base-size-2) var(--base-size-6);
position: relative;
align-items: center;

&:not(:disabled, [aria-disabled='true']) {
--button-invisible-iconColor-rest: var(--fgColor-link);
}

[data-component='Octicon'] {
/* Set icon size to text size to exactly match the branch name component */
height: var(--text-body-size-small);
width: var(--text-body-size-small);
}

/* Dividing line between button and name. */
&::before {
content: '';
position: absolute;
width: var(--borderWidth-default);
/* Position _between_ items (offsetting by width) to avoid overlapping with the real button border on hover/focus */
inset: var(--base-size-4) auto var(--base-size-4) calc(-1 * var(--borderWidth-default));
/* we want a divider/border but are using background color to make that happen */
/* stylelint-disable-next-line primer/colors */
background-color: var(--borderColor-accent-muted);
}
}
}
Loading
Loading