Skip to content

Commit 67fff83

Browse files
authored
Fix river breakout tabs custom background (#1375)
* fix river breakout tabs background bug * fix pr feedback * restore full-bleed option * rename full bleed prop * update snapshots
1 parent 99af142 commit 67fff83

21 files changed

Lines changed: 437 additions & 110 deletions
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
'@primer/react-brand': minor
3+
---
4+
5+
Added a `backgroundVisual` prop to `RiverBreakoutTabs` for rendering a decorative background component. The background persists across tab changes.
6+
7+
Also added an `imagePosition` prop to inset and align the media over the `backgroundVisual`.
8+
9+
Also added a `backgroundVisualFullBleed` prop to render the `backgroundVisual` contained (default) or full-bleed to the gridline edges.
10+
11+
```tsx
12+
<RiverBreakoutTabs backgroundVisual={<MyCustomBackground />} imagePosition="block-end">
13+
<RiverBreakoutTabs.A11yHeading>Workflows</RiverBreakoutTabs.A11yHeading>
14+
{/* ...items... */}
15+
</RiverBreakoutTabs>
16+
```

apps/next-docs/content/components/RiverBreakoutTabs/index.mdx

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -63,16 +63,29 @@ import {RiverBreakoutTabs} from '@primer/react-brand'
6363

6464
```jsx live
6565
<Section>
66-
<RiverBreakoutTabs>
67-
<RiverBreakoutTabs.A11yHeading>Breakout with a padded background</RiverBreakoutTabs.A11yHeading>
66+
<RiverBreakoutTabs
67+
backgroundVisual={
68+
<div
69+
style={{
70+
position: 'absolute',
71+
inset: 0,
72+
backgroundColor: 'var(--base-color-scale-green-0)',
73+
backgroundImage: 'radial-gradient(var(--base-color-scale-green-2) 1px, transparent 3px)',
74+
backgroundSize: '10px 10px',
75+
}}
76+
/>
77+
}
78+
imagePosition="block-end"
79+
>
80+
<RiverBreakoutTabs.A11yHeading>Breakout with a decorative background</RiverBreakoutTabs.A11yHeading>
6881

6982
<RiverBreakoutTabs.Item>
7083
<RiverBreakoutTabs.Heading>Plan</RiverBreakoutTabs.Heading>
7184
<RiverBreakoutTabs.Content>
7285
<Text>Start from issue context and turn goals into task lists.</Text>
7386
<Link href="#">Open planning</Link>
7487
</RiverBreakoutTabs.Content>
75-
<RiverBreakoutTabs.Visual imageBackgroundColor="subtle">
88+
<RiverBreakoutTabs.Visual>
7689
<img src="/images/placeholder-1.png" alt="Planning tab visual" />
7790
</RiverBreakoutTabs.Visual>
7891
</RiverBreakoutTabs.Item>
@@ -83,7 +96,7 @@ import {RiverBreakoutTabs} from '@primer/react-brand'
8396
<Text>Carry changes through review and checks to production.</Text>
8497
<Link href="#">Open delivery flow</Link>
8598
</RiverBreakoutTabs.Content>
86-
<RiverBreakoutTabs.Visual imageBackgroundColor="subtle">
99+
<RiverBreakoutTabs.Visual>
87100
<img src="/images/placeholder-2.png" alt="Delivery tab visual" />
88101
</RiverBreakoutTabs.Visual>
89102
</RiverBreakoutTabs.Item>
@@ -95,12 +108,15 @@ import {RiverBreakoutTabs} from '@primer/react-brand'
95108

96109
### RiverBreakoutTabs <Label>Required</Label>
97110

98-
| Name | Type | Default | Description |
99-
| :--------------------- | :-------------------------------- | :-----: | :-------------------------------------------------------------------------- |
100-
| `children` | `RiverBreakoutTabs.Item[]` | | Includes one required `RiverBreakoutTabs.A11yHeading` and one or more items |
101-
| `defaultSelectedIndex` | `number` | `0` | Default selected index for uncontrolled mode |
102-
| `selectedIndex` | `number` | | Selected index for controlled mode |
103-
| `onChange` | `(selectedIndex: number) => void` | | Callback called when selected index changes |
111+
| Name | Type | Default | Description |
112+
| :-------------------------- | :-------------------------------- | :--------: | :--------------------------------------------------------------------------------------------------------------------------------- |
113+
| `children` | `RiverBreakoutTabs.Item[]` | | Includes one required `RiverBreakoutTabs.A11yHeading` and one or more items |
114+
| `defaultSelectedIndex` | `number` | `0` | Default selected index for uncontrolled mode |
115+
| `selectedIndex` | `number` | | Selected index for controlled mode |
116+
| `onChange` | `(selectedIndex: number) => void` | | Callback called when selected index changes |
117+
| `backgroundVisual` | `ReactNode` | | Decorative `aria-hidden` visual behind the shared visual region; persists across tab changes. Must not contain interactive content |
118+
| `imagePosition` | `'center' \| 'block-end'` | `'center'` | Positions the media over `backgroundVisual`; `block-end` pins it to the bottom edge |
119+
| `backgroundVisualFullBleed` | `boolean` | `false` | When `true`, the `backgroundVisual` bleeds out to the gridline edges instead of being inset within them |
104120

105121
`RiverBreakoutTabs` extends the HTML `section` element and supports all `section` props.
106122

packages/react/src/Hero/Hero.examples.stories.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,7 @@ export const GridlineExpressiveBlockEndPaddedTrailingComponent: Story = {
301301
enableAnimation
302302
variant="gridline-expressive"
303303
trailingComponent={() => (
304-
<Text>
304+
<Text variant="muted">
305305
Already have Visual Studio Code? <InlineLink href="#">Open now</InlineLink>
306306
</Text>
307307
)}
726 Bytes
Loading

packages/react/src/river/RiverBreakoutTabs/RiverBreakoutTabs.examples.stories.tsx

Lines changed: 38 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {useEffect} from 'react'
1+
import {useEffect, useState} from 'react'
22
import type {Meta, StoryObj} from '@storybook/react'
33
import {AiModelIcon, CopilotIcon, ShieldCheckIcon} from '@primer/octicons-react'
44
import {useTranslation} from 'react-i18next'
@@ -9,7 +9,6 @@ import renderUI2 from '../../fixtures/images/copilot-vscode-agent-mode-2.png'
99
import renderUI3 from '../../fixtures/images/copilot-vscode-agent-mode-3.png'
1010
import placeholderBg from '../../fixtures/images/dither-bg-landscape-green.png'
1111
import posterImage from '../../fixtures/images/example-poster.png'
12-
import {useWindowSize} from '../../hooks/useWindowSize'
1312
import {VideoPlayer} from '../../VideoPlayer'
1413

1514
const meta = {
@@ -24,29 +23,37 @@ export default meta
2423

2524
type Story = StoryObj<typeof RiverBreakoutTabs>
2625

26+
function SharedDitherBackdrop() {
27+
const [shown, setShown] = useState(false)
28+
29+
useEffect(() => {
30+
const raf = requestAnimationFrame(() => setShown(true))
31+
return () => cancelAnimationFrame(raf)
32+
}, [])
33+
34+
return (
35+
<div
36+
style={{
37+
position: 'absolute',
38+
inset: 0,
39+
backgroundImage: `url(${placeholderBg})`,
40+
backgroundSize: 'cover',
41+
backgroundPosition: 'center',
42+
opacity: shown ? 1 : 0,
43+
transition: 'opacity 1.5s ease-out',
44+
}}
45+
/>
46+
)
47+
}
48+
2749
export const WithImages: Story = {
2850
name: 'With images',
2951
render: function WithImagesRender() {
3052
const {t} = useTranslation('RiverBreakoutTabs')
31-
const {isLarge} = useWindowSize()
32-
33-
useEffect(() => {
34-
const visuals = Array.from(document.querySelectorAll('[data-river-tabs-dither-bg]')).filter(
35-
(value): value is HTMLDivElement => value instanceof HTMLDivElement,
36-
)
37-
38-
const cleanups = visuals.map(visualElement => TempFadeInBackgroundEffect(visualElement, placeholderBg, 500))
39-
40-
return () => {
41-
for (const cleanup of cleanups) {
42-
cleanup()
43-
}
44-
}
45-
}, [isLarge])
4653

4754
return (
4855
<Section>
49-
<RiverBreakoutTabs>
56+
<RiverBreakoutTabs backgroundVisual={<SharedDitherBackdrop />} imagePosition="block-end">
5057
<RiverBreakoutTabs.A11yHeading>{t('with_images_a11y_heading')}</RiverBreakoutTabs.A11yHeading>
5158

5259
<RiverBreakoutTabs.Item>
@@ -56,7 +63,7 @@ export const WithImages: Story = {
5663
<Text>{t('with_images_item_plan_body')}</Text>
5764
<Link href="https://github.com/features/copilot">{t('with_images_item_plan_link')}</Link>
5865
</RiverBreakoutTabs.Content>
59-
<RiverBreakoutTabs.Visual data-river-tabs-dither-bg imageBackgroundColor="subtle">
66+
<RiverBreakoutTabs.Visual>
6067
<Image src={renderUI1} alt={t('alt_ai_plan_editor')} />
6168
</RiverBreakoutTabs.Visual>
6269
</RiverBreakoutTabs.Item>
@@ -68,7 +75,7 @@ export const WithImages: Story = {
6875
<Text>{t('with_images_item_refine_body')}</Text>
6976
<Link href="https://github.com/features/copilot/chat">{t('with_images_item_refine_link')}</Link>
7077
</RiverBreakoutTabs.Content>
71-
<RiverBreakoutTabs.Visual data-river-tabs-dither-bg imageBackgroundColor="subtle">
78+
<RiverBreakoutTabs.Visual>
7279
<Image src={renderUI2} alt={t('alt_chat_guiding_edits')} />
7380
</RiverBreakoutTabs.Visual>
7481
</RiverBreakoutTabs.Item>
@@ -80,7 +87,7 @@ export const WithImages: Story = {
8087
<Text>{t('with_images_item_merge_body')}</Text>
8188
<Link href="https://github.com/features/copilot/plans">{t('with_images_item_merge_link')}</Link>
8289
</RiverBreakoutTabs.Content>
83-
<RiverBreakoutTabs.Visual data-river-tabs-dither-bg imageBackgroundColor="subtle">
90+
<RiverBreakoutTabs.Visual>
8491
<Image src={renderUI3} alt={t('alt_checks_before_merge')} />
8592
</RiverBreakoutTabs.Visual>
8693
</RiverBreakoutTabs.Item>
@@ -94,25 +101,10 @@ export const WithVideos: Story = {
94101
name: 'With videos',
95102
render: function WithVideosRender() {
96103
const {t} = useTranslation('RiverBreakoutTabs')
97-
const {isLarge} = useWindowSize()
98-
99-
useEffect(() => {
100-
const visuals = Array.from(document.querySelectorAll('[data-river-tabs-dither-bg]')).filter(
101-
(value): value is HTMLDivElement => value instanceof HTMLDivElement,
102-
)
103-
104-
const cleanups = visuals.map(visualElement => TempFadeInBackgroundEffect(visualElement, placeholderBg, 500))
105-
106-
return () => {
107-
for (const cleanup of cleanups) {
108-
cleanup()
109-
}
110-
}
111-
}, [isLarge])
112104

113105
return (
114106
<Section>
115-
<RiverBreakoutTabs>
107+
<RiverBreakoutTabs backgroundVisual={<SharedDitherBackdrop />} imagePosition="block-end">
116108
<RiverBreakoutTabs.A11yHeading>{t('with_videos_a11y_heading')}</RiverBreakoutTabs.A11yHeading>
117109

118110
<RiverBreakoutTabs.Item>
@@ -122,7 +114,7 @@ export const WithVideos: Story = {
122114
<Text>{t('with_videos_item_backlog_body')}</Text>
123115
<Link href="https://github.com/features/copilot">{t('with_videos_item_backlog_link')}</Link>
124116
</RiverBreakoutTabs.Content>
125-
<RiverBreakoutTabs.Visual data-river-tabs-dither-bg imageBackgroundColor="subtle">
117+
<RiverBreakoutTabs.Visual>
126118
<VideoPlayer title={t('video_title_planning')} poster={posterImage} style={{height: '100%'}}>
127119
<VideoPlayer.Source src="./example.mp4" type="video/mp4" />
128120
<VideoPlayer.Track src="./example.vtt" default />
@@ -137,7 +129,7 @@ export const WithVideos: Story = {
137129
<Text>{t('with_videos_item_workflow_body')}</Text>
138130
<Link href="https://github.com/features/copilot/chat">{t('with_videos_item_workflow_link')}</Link>
139131
</RiverBreakoutTabs.Content>
140-
<RiverBreakoutTabs.Visual data-river-tabs-dither-bg imageBackgroundColor="subtle">
132+
<RiverBreakoutTabs.Visual>
141133
<VideoPlayer title={t('video_title_coding')} poster={posterImage} style={{height: '100%'}}>
142134
<VideoPlayer.Source src="./example.mp4" type="video/mp4" />
143135
<VideoPlayer.Track src="./example.vtt" default />
@@ -152,7 +144,7 @@ export const WithVideos: Story = {
152144
<Text>{t('with_videos_item_confidence_body')}</Text>
153145
<Link href="https://github.com/features/copilot/plans">{t('with_videos_item_confidence_link')}</Link>
154146
</RiverBreakoutTabs.Content>
155-
<RiverBreakoutTabs.Visual data-river-tabs-dither-bg imageBackgroundColor="subtle">
147+
<RiverBreakoutTabs.Visual>
156148
<VideoPlayer title={t('video_title_merge_confidence')} poster={posterImage} style={{height: '100%'}}>
157149
<VideoPlayer.Source src="./example.mp4" type="video/mp4" />
158150
<VideoPlayer.Track src="./example.vtt" default />
@@ -165,66 +157,16 @@ export const WithVideos: Story = {
165157
},
166158
}
167159

168-
/**
169-
* This helper mirrors the temporary fade-in background approach used in Hero and River examples.
170-
*/
171-
function TempFadeInBackgroundEffect(element: HTMLDivElement, backgroundImageUrl: string, delay = 100) {
172-
element.style.position = 'relative'
173-
174-
const bgDiv = document.createElement('div')
175-
bgDiv.style.cssText = `
176-
position: absolute;
177-
inset: 0;
178-
background-image: url(${backgroundImageUrl});
179-
background-size: cover;
180-
background-position: center;
181-
opacity: 0;
182-
transition: opacity 1.5s ease-out;
183-
z-index: 0;
184-
`
185-
element.insertBefore(bgDiv, element.firstChild)
186-
187-
const children = element.children
188-
for (let i = 1; i < children.length; i++) {
189-
;(children[i] as HTMLElement).style.position = 'relative'
190-
;(children[i] as HTMLElement).style.zIndex = '1'
191-
}
192-
193-
const timer = setTimeout(() => {
194-
bgDiv.style.opacity = '1'
195-
}, delay)
196-
197-
return () => {
198-
clearTimeout(timer)
199-
bgDiv.remove()
200-
}
201-
}
202-
203160
export const WithRivers: Story = {
204161
name: 'With rivers',
205162
render: function WithRiversRender() {
206163
const {t} = useTranslation('RiverBreakoutTabs')
207-
const {isLarge} = useWindowSize()
208-
209-
useEffect(() => {
210-
const visuals = Array.from(document.querySelectorAll('[data-river-tabs-dither-bg]')).filter(
211-
(value): value is HTMLDivElement => value instanceof HTMLDivElement,
212-
)
213-
214-
const cleanups = visuals.map(visualElement => TempFadeInBackgroundEffect(visualElement, placeholderBg, 500))
215-
216-
return () => {
217-
for (const cleanup of cleanups) {
218-
cleanup()
219-
}
220-
}
221-
}, [isLarge])
222164

223165
return (
224166
<Section>
225167
<Stack padding="none" gap={64}>
226168
<River variant="gridline">
227-
<River.Visual data-river-tabs-dither-bg imageBackgroundColor="subtle">
169+
<River.Visual imageBackgroundColor="subtle">
228170
<Image src={renderUI1} alt={t('with_rivers_top_visual_alt')} />
229171
</River.Visual>
230172
<River.Content>
@@ -233,7 +175,7 @@ export const WithRivers: Story = {
233175
</River.Content>
234176
</River>
235177

236-
<RiverBreakoutTabs>
178+
<RiverBreakoutTabs backgroundVisual={<SharedDitherBackdrop />} imagePosition="block-end">
237179
<RiverBreakoutTabs.A11yHeading>{t('with_images_a11y_heading')}</RiverBreakoutTabs.A11yHeading>
238180

239181
<RiverBreakoutTabs.Item>
@@ -243,7 +185,7 @@ export const WithRivers: Story = {
243185
<Text>{t('with_images_item_plan_body')}</Text>
244186
<Link href="https://github.com/features/copilot">{t('with_images_item_plan_link')}</Link>
245187
</RiverBreakoutTabs.Content>
246-
<RiverBreakoutTabs.Visual data-river-tabs-dither-bg imageBackgroundColor="subtle">
188+
<RiverBreakoutTabs.Visual>
247189
<Image src={renderUI1} alt={t('alt_ai_plan_editor')} />
248190
</RiverBreakoutTabs.Visual>
249191
</RiverBreakoutTabs.Item>
@@ -255,7 +197,7 @@ export const WithRivers: Story = {
255197
<Text>{t('with_images_item_refine_body')}</Text>
256198
<Link href="https://github.com/features/copilot/chat">{t('with_images_item_refine_link')}</Link>
257199
</RiverBreakoutTabs.Content>
258-
<RiverBreakoutTabs.Visual data-river-tabs-dither-bg imageBackgroundColor="subtle">
200+
<RiverBreakoutTabs.Visual>
259201
<Image src={renderUI2} alt={t('alt_chat_guiding_edits')} />
260202
</RiverBreakoutTabs.Visual>
261203
</RiverBreakoutTabs.Item>
@@ -267,14 +209,14 @@ export const WithRivers: Story = {
267209
<Text>{t('with_images_item_merge_body')}</Text>
268210
<Link href="https://github.com/features/copilot/plans">{t('with_images_item_merge_link')}</Link>
269211
</RiverBreakoutTabs.Content>
270-
<RiverBreakoutTabs.Visual data-river-tabs-dither-bg imageBackgroundColor="subtle">
212+
<RiverBreakoutTabs.Visual>
271213
<Image src={renderUI3} alt={t('alt_checks_before_merge')} />
272214
</RiverBreakoutTabs.Visual>
273215
</RiverBreakoutTabs.Item>
274216
</RiverBreakoutTabs>
275217

276218
<River variant="gridline">
277-
<River.Visual data-river-tabs-dither-bg imageBackgroundColor="subtle">
219+
<River.Visual imageBackgroundColor="subtle">
278220
<Image src={renderUI3} alt={t('with_rivers_bottom_visual_alt')} />
279221
</River.Visual>
280222
<River.Content>

0 commit comments

Comments
 (0)