Skip to content
Merged
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
16 changes: 16 additions & 0 deletions .changeset/river-breakout-tabs-background-visual.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
'@primer/react-brand': minor
---

Added a `backgroundVisual` prop to `RiverBreakoutTabs` for rendering a decorative background component. The background persists across tab changes.

Also added an `imagePosition` prop to inset and align the media over the `backgroundVisual`.

Also added a `backgroundVisualFullBleed` prop to render the `backgroundVisual` contained (default) or full-bleed to the gridline edges.

```tsx
<RiverBreakoutTabs backgroundVisual={<MyCustomBackground />} imagePosition="block-end">
<RiverBreakoutTabs.A11yHeading>Workflows</RiverBreakoutTabs.A11yHeading>
{/* ...items... */}
</RiverBreakoutTabs>
```
36 changes: 26 additions & 10 deletions apps/next-docs/content/components/RiverBreakoutTabs/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -63,16 +63,29 @@ import {RiverBreakoutTabs} from '@primer/react-brand'

```jsx live
<Section>
<RiverBreakoutTabs>
<RiverBreakoutTabs.A11yHeading>Breakout with a padded background</RiverBreakoutTabs.A11yHeading>
<RiverBreakoutTabs
backgroundVisual={
<div
style={{
position: 'absolute',
inset: 0,
backgroundColor: 'var(--base-color-scale-green-0)',
backgroundImage: 'radial-gradient(var(--base-color-scale-green-2) 1px, transparent 3px)',
backgroundSize: '10px 10px',
}}
/>
}
imagePosition="block-end"
>
<RiverBreakoutTabs.A11yHeading>Breakout with a decorative background</RiverBreakoutTabs.A11yHeading>

<RiverBreakoutTabs.Item>
<RiverBreakoutTabs.Heading>Plan</RiverBreakoutTabs.Heading>
<RiverBreakoutTabs.Content>
<Text>Start from issue context and turn goals into task lists.</Text>
<Link href="#">Open planning</Link>
</RiverBreakoutTabs.Content>
<RiverBreakoutTabs.Visual imageBackgroundColor="subtle">
<RiverBreakoutTabs.Visual>
<img src="/images/placeholder-1.png" alt="Planning tab visual" />
</RiverBreakoutTabs.Visual>
</RiverBreakoutTabs.Item>
Expand All @@ -83,7 +96,7 @@ import {RiverBreakoutTabs} from '@primer/react-brand'
<Text>Carry changes through review and checks to production.</Text>
<Link href="#">Open delivery flow</Link>
</RiverBreakoutTabs.Content>
<RiverBreakoutTabs.Visual imageBackgroundColor="subtle">
<RiverBreakoutTabs.Visual>
<img src="/images/placeholder-2.png" alt="Delivery tab visual" />
</RiverBreakoutTabs.Visual>
</RiverBreakoutTabs.Item>
Expand All @@ -95,12 +108,15 @@ import {RiverBreakoutTabs} from '@primer/react-brand'

### RiverBreakoutTabs <Label>Required</Label>

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

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

Expand Down
2 changes: 1 addition & 1 deletion packages/react/src/Hero/Hero.examples.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@ export const GridlineExpressiveBlockEndPaddedTrailingComponent: Story = {
enableAnimation
variant="gridline-expressive"
trailingComponent={() => (
<Text>
<Text variant="muted">
Already have Visual Studio Code? <InlineLink href="#">Open now</InlineLink>
</Text>
)}
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {useEffect} from 'react'
import {useEffect, useState} from 'react'
import type {Meta, StoryObj} from '@storybook/react'
import {AiModelIcon, CopilotIcon, ShieldCheckIcon} from '@primer/octicons-react'
import {useTranslation} from 'react-i18next'
Expand All @@ -9,7 +9,6 @@ import renderUI2 from '../../fixtures/images/copilot-vscode-agent-mode-2.png'
import renderUI3 from '../../fixtures/images/copilot-vscode-agent-mode-3.png'
import placeholderBg from '../../fixtures/images/dither-bg-landscape-green.png'
import posterImage from '../../fixtures/images/example-poster.png'
import {useWindowSize} from '../../hooks/useWindowSize'
import {VideoPlayer} from '../../VideoPlayer'

const meta = {
Expand All @@ -24,29 +23,37 @@ export default meta

type Story = StoryObj<typeof RiverBreakoutTabs>

function SharedDitherBackdrop() {
const [shown, setShown] = useState(false)

useEffect(() => {
const raf = requestAnimationFrame(() => setShown(true))
return () => cancelAnimationFrame(raf)
}, [])

return (
<div
style={{
position: 'absolute',
inset: 0,
backgroundImage: `url(${placeholderBg})`,
backgroundSize: 'cover',
backgroundPosition: 'center',
opacity: shown ? 1 : 0,
transition: 'opacity 1.5s ease-out',
}}
/>
)
}

export const WithImages: Story = {
name: 'With images',
render: function WithImagesRender() {
const {t} = useTranslation('RiverBreakoutTabs')
const {isLarge} = useWindowSize()

useEffect(() => {
const visuals = Array.from(document.querySelectorAll('[data-river-tabs-dither-bg]')).filter(
(value): value is HTMLDivElement => value instanceof HTMLDivElement,
)

const cleanups = visuals.map(visualElement => TempFadeInBackgroundEffect(visualElement, placeholderBg, 500))

return () => {
for (const cleanup of cleanups) {
cleanup()
}
}
}, [isLarge])

return (
<Section>
<RiverBreakoutTabs>
<RiverBreakoutTabs backgroundVisual={<SharedDitherBackdrop />} imagePosition="block-end">
<RiverBreakoutTabs.A11yHeading>{t('with_images_a11y_heading')}</RiverBreakoutTabs.A11yHeading>

<RiverBreakoutTabs.Item>
Expand All @@ -56,7 +63,7 @@ export const WithImages: Story = {
<Text>{t('with_images_item_plan_body')}</Text>
<Link href="https://github.com/features/copilot">{t('with_images_item_plan_link')}</Link>
</RiverBreakoutTabs.Content>
<RiverBreakoutTabs.Visual data-river-tabs-dither-bg imageBackgroundColor="subtle">
<RiverBreakoutTabs.Visual>
<Image src={renderUI1} alt={t('alt_ai_plan_editor')} />
</RiverBreakoutTabs.Visual>
</RiverBreakoutTabs.Item>
Expand All @@ -68,7 +75,7 @@ export const WithImages: Story = {
<Text>{t('with_images_item_refine_body')}</Text>
<Link href="https://github.com/features/copilot/chat">{t('with_images_item_refine_link')}</Link>
</RiverBreakoutTabs.Content>
<RiverBreakoutTabs.Visual data-river-tabs-dither-bg imageBackgroundColor="subtle">
<RiverBreakoutTabs.Visual>
<Image src={renderUI2} alt={t('alt_chat_guiding_edits')} />
</RiverBreakoutTabs.Visual>
</RiverBreakoutTabs.Item>
Expand All @@ -80,7 +87,7 @@ export const WithImages: Story = {
<Text>{t('with_images_item_merge_body')}</Text>
<Link href="https://github.com/features/copilot/plans">{t('with_images_item_merge_link')}</Link>
</RiverBreakoutTabs.Content>
<RiverBreakoutTabs.Visual data-river-tabs-dither-bg imageBackgroundColor="subtle">
<RiverBreakoutTabs.Visual>
<Image src={renderUI3} alt={t('alt_checks_before_merge')} />
</RiverBreakoutTabs.Visual>
</RiverBreakoutTabs.Item>
Expand All @@ -94,25 +101,10 @@ export const WithVideos: Story = {
name: 'With videos',
render: function WithVideosRender() {
const {t} = useTranslation('RiverBreakoutTabs')
const {isLarge} = useWindowSize()

useEffect(() => {
const visuals = Array.from(document.querySelectorAll('[data-river-tabs-dither-bg]')).filter(
(value): value is HTMLDivElement => value instanceof HTMLDivElement,
)

const cleanups = visuals.map(visualElement => TempFadeInBackgroundEffect(visualElement, placeholderBg, 500))

return () => {
for (const cleanup of cleanups) {
cleanup()
}
}
}, [isLarge])

return (
<Section>
<RiverBreakoutTabs>
<RiverBreakoutTabs backgroundVisual={<SharedDitherBackdrop />} imagePosition="block-end">
<RiverBreakoutTabs.A11yHeading>{t('with_videos_a11y_heading')}</RiverBreakoutTabs.A11yHeading>

<RiverBreakoutTabs.Item>
Expand All @@ -122,7 +114,7 @@ export const WithVideos: Story = {
<Text>{t('with_videos_item_backlog_body')}</Text>
<Link href="https://github.com/features/copilot">{t('with_videos_item_backlog_link')}</Link>
</RiverBreakoutTabs.Content>
<RiverBreakoutTabs.Visual data-river-tabs-dither-bg imageBackgroundColor="subtle">
<RiverBreakoutTabs.Visual>
<VideoPlayer title={t('video_title_planning')} poster={posterImage} style={{height: '100%'}}>
<VideoPlayer.Source src="./example.mp4" type="video/mp4" />
<VideoPlayer.Track src="./example.vtt" default />
Expand All @@ -137,7 +129,7 @@ export const WithVideos: Story = {
<Text>{t('with_videos_item_workflow_body')}</Text>
<Link href="https://github.com/features/copilot/chat">{t('with_videos_item_workflow_link')}</Link>
</RiverBreakoutTabs.Content>
<RiverBreakoutTabs.Visual data-river-tabs-dither-bg imageBackgroundColor="subtle">
<RiverBreakoutTabs.Visual>
<VideoPlayer title={t('video_title_coding')} poster={posterImage} style={{height: '100%'}}>
<VideoPlayer.Source src="./example.mp4" type="video/mp4" />
<VideoPlayer.Track src="./example.vtt" default />
Expand All @@ -152,7 +144,7 @@ export const WithVideos: Story = {
<Text>{t('with_videos_item_confidence_body')}</Text>
<Link href="https://github.com/features/copilot/plans">{t('with_videos_item_confidence_link')}</Link>
</RiverBreakoutTabs.Content>
<RiverBreakoutTabs.Visual data-river-tabs-dither-bg imageBackgroundColor="subtle">
<RiverBreakoutTabs.Visual>
<VideoPlayer title={t('video_title_merge_confidence')} poster={posterImage} style={{height: '100%'}}>
<VideoPlayer.Source src="./example.mp4" type="video/mp4" />
<VideoPlayer.Track src="./example.vtt" default />
Expand All @@ -165,66 +157,16 @@ export const WithVideos: Story = {
},
}

/**
* This helper mirrors the temporary fade-in background approach used in Hero and River examples.
*/
function TempFadeInBackgroundEffect(element: HTMLDivElement, backgroundImageUrl: string, delay = 100) {
element.style.position = 'relative'

const bgDiv = document.createElement('div')
bgDiv.style.cssText = `
position: absolute;
inset: 0;
background-image: url(${backgroundImageUrl});
background-size: cover;
background-position: center;
opacity: 0;
transition: opacity 1.5s ease-out;
z-index: 0;
`
element.insertBefore(bgDiv, element.firstChild)

const children = element.children
for (let i = 1; i < children.length; i++) {
;(children[i] as HTMLElement).style.position = 'relative'
;(children[i] as HTMLElement).style.zIndex = '1'
}

const timer = setTimeout(() => {
bgDiv.style.opacity = '1'
}, delay)

return () => {
clearTimeout(timer)
bgDiv.remove()
}
}

export const WithRivers: Story = {
name: 'With rivers',
render: function WithRiversRender() {
const {t} = useTranslation('RiverBreakoutTabs')
const {isLarge} = useWindowSize()

useEffect(() => {
const visuals = Array.from(document.querySelectorAll('[data-river-tabs-dither-bg]')).filter(
(value): value is HTMLDivElement => value instanceof HTMLDivElement,
)

const cleanups = visuals.map(visualElement => TempFadeInBackgroundEffect(visualElement, placeholderBg, 500))

return () => {
for (const cleanup of cleanups) {
cleanup()
}
}
}, [isLarge])

return (
<Section>
<Stack padding="none" gap={64}>
<River variant="gridline">
<River.Visual data-river-tabs-dither-bg imageBackgroundColor="subtle">
<River.Visual imageBackgroundColor="subtle">
<Image src={renderUI1} alt={t('with_rivers_top_visual_alt')} />
</River.Visual>
<River.Content>
Expand All @@ -233,7 +175,7 @@ export const WithRivers: Story = {
</River.Content>
</River>

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

<RiverBreakoutTabs.Item>
Expand All @@ -243,7 +185,7 @@ export const WithRivers: Story = {
<Text>{t('with_images_item_plan_body')}</Text>
<Link href="https://github.com/features/copilot">{t('with_images_item_plan_link')}</Link>
</RiverBreakoutTabs.Content>
<RiverBreakoutTabs.Visual data-river-tabs-dither-bg imageBackgroundColor="subtle">
<RiverBreakoutTabs.Visual>
<Image src={renderUI1} alt={t('alt_ai_plan_editor')} />
</RiverBreakoutTabs.Visual>
</RiverBreakoutTabs.Item>
Expand All @@ -255,7 +197,7 @@ export const WithRivers: Story = {
<Text>{t('with_images_item_refine_body')}</Text>
<Link href="https://github.com/features/copilot/chat">{t('with_images_item_refine_link')}</Link>
</RiverBreakoutTabs.Content>
<RiverBreakoutTabs.Visual data-river-tabs-dither-bg imageBackgroundColor="subtle">
<RiverBreakoutTabs.Visual>
<Image src={renderUI2} alt={t('alt_chat_guiding_edits')} />
</RiverBreakoutTabs.Visual>
</RiverBreakoutTabs.Item>
Expand All @@ -267,14 +209,14 @@ export const WithRivers: Story = {
<Text>{t('with_images_item_merge_body')}</Text>
<Link href="https://github.com/features/copilot/plans">{t('with_images_item_merge_link')}</Link>
</RiverBreakoutTabs.Content>
<RiverBreakoutTabs.Visual data-river-tabs-dither-bg imageBackgroundColor="subtle">
<RiverBreakoutTabs.Visual>
<Image src={renderUI3} alt={t('alt_checks_before_merge')} />
</RiverBreakoutTabs.Visual>
</RiverBreakoutTabs.Item>
</RiverBreakoutTabs>

<River variant="gridline">
<River.Visual data-river-tabs-dither-bg imageBackgroundColor="subtle">
<River.Visual imageBackgroundColor="subtle">
<Image src={renderUI3} alt={t('with_rivers_bottom_visual_alt')} />
</River.Visual>
<River.Content>
Expand Down
Loading
Loading