Skip to content

Commit c4dbb5b

Browse files
authored
Merge branch 'main' into banner-fix
2 parents 5fe3b93 + 52e3f8d commit c4dbb5b

File tree

7 files changed

+67
-78
lines changed

7 files changed

+67
-78
lines changed

.changeset/dark-birds-dont.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@primer/react': patch
3+
---
4+
5+
Fixes `Details` flickering, prevents re-renders

examples/codesandbox/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,6 @@
2020
"@primer/react": "38.0.0-rc.8",
2121
"styled-components": "5.x",
2222
"typescript": "^5.9.2",
23-
"vite": "^7.1.5"
23+
"vite": "^7.1.11"
2424
}
2525
}

package-lock.json

Lines changed: 12 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/react/src/ButtonGroup/ButtonGroup.stories.tsx

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,20 @@ export const Default = () => (
2323
</ButtonGroup>
2424
)
2525

26-
export const Playground: StoryFn<ButtonProps> = args => (
27-
<ButtonGroup>
28-
<Button {...args}>Button 1</Button>
29-
<Button {...args}>Button 2</Button>
30-
<Button {...args}>Button 3</Button>
31-
</ButtonGroup>
32-
)
26+
export const Playground: StoryFn<ButtonProps & {buttonCount: number}> = args => {
27+
const {buttonCount = 3, ...buttonProps} = args
28+
const buttons = Array.from({length: buttonCount}, (_, i) => (
29+
<Button key={i} {...buttonProps}>
30+
{`Button ${i + 1}`}
31+
</Button>
32+
))
33+
34+
return <ButtonGroup>{buttons}</ButtonGroup>
35+
}
3336
Playground.args = {
3437
size: 'medium',
3538
disabled: false,
39+
buttonCount: 3,
3640
}
3741
Playground.argTypes = {
3842
size: {
@@ -46,4 +50,13 @@ Playground.argTypes = {
4650
type: 'boolean',
4751
},
4852
},
53+
buttonCount: {
54+
control: {
55+
type: 'number',
56+
min: 2,
57+
max: 6,
58+
step: 1,
59+
},
60+
description: 'Number of buttons in the group (2-6)',
61+
},
4962
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import type {StoryFn, Meta} from '@storybook/react-vite'
2+
import Details from './Details'
3+
import useDetails from '../hooks/useDetails'
4+
5+
export default {
6+
title: 'Components/Details/Features',
7+
component: Details,
8+
} as Meta<typeof Details>
9+
10+
export const WithCustomSummary: StoryFn<typeof Details> = () => {
11+
const {getDetailsProps} = useDetails({closeOnOutsideClick: true})
12+
return (
13+
<Details {...getDetailsProps()}>
14+
<summary>Custom see Details</summary>
15+
This is some content
16+
</Details>
17+
)
18+
}

packages/react/src/Details/Details.tsx

Lines changed: 11 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import React, {useEffect, useState, type ComponentPropsWithoutRef, type ReactElement} from 'react'
1+
import React, {useEffect, type ComponentPropsWithoutRef, type ReactElement} from 'react'
2+
import {warning} from '../utils/warning'
23
import {clsx} from 'clsx'
34
import classes from './Details.module.css'
45
import {useMergedRefs} from '../internal/hooks/useMergedRefs'
@@ -7,40 +8,25 @@ const Root = React.forwardRef<HTMLDetailsElement, DetailsProps>(
78
({className, children, ...rest}, forwardRef): ReactElement => {
89
const detailsRef = React.useRef<HTMLDetailsElement>(null)
910
const ref = useMergedRefs(forwardRef, detailsRef)
10-
const [hasSummary, setHasSummary] = useState(false)
1111

1212
useEffect(() => {
13-
const {current: details} = detailsRef
14-
if (!details) {
13+
if (!__DEV__) {
1514
return
1615
}
1716

18-
const updateSummary = () => {
19-
const summary = details.querySelector('summary:not([data-default-summary])')
20-
setHasSummary(!!summary)
21-
}
22-
23-
// Update summary on mount
24-
updateSummary()
25-
26-
const observer = new MutationObserver(() => {
27-
updateSummary()
28-
})
29-
30-
observer.observe(details, {
31-
childList: true,
32-
subtree: true,
33-
})
34-
35-
return () => {
36-
observer.disconnect()
17+
const {current: details} = detailsRef
18+
if (!details) {
19+
return
3720
}
21+
const summary = details.querySelector('summary:not([data-default-summary])')
22+
warning(
23+
summary === null,
24+
'The <Details> component must have a <summary> child component. You can either use <Details.Summary> or a native <summary> element.',
25+
)
3826
}, [])
3927

4028
return (
4129
<details className={clsx(className, classes.Details)} {...rest} ref={ref}>
42-
{/* Include default summary if summary is not provided */}
43-
{!hasSummary && <Details.Summary data-default-summary>{'See Details'}</Details.Summary>}
4430
{children}
4531
</details>
4632
)

packages/react/src/Details/__tests__/Details.test.tsx

Lines changed: 0 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -82,39 +82,6 @@ describe('Details', () => {
8282
expect(getByTestId('summary')).toHaveTextContent('Open')
8383
})
8484

85-
it('Adds default summary if no summary supplied', async () => {
86-
const {getByText} = render(<Details data-testid="details">content</Details>)
87-
88-
expect(getByText('See Details')).toBeInTheDocument()
89-
expect(getByText('See Details').tagName).toBe('SUMMARY')
90-
})
91-
92-
it('Does not add default summary if summary supplied', async () => {
93-
const {findByTestId, findByText} = render(
94-
<Details data-testid="details">
95-
<Details.Summary data-testid="summary">summary</Details.Summary>
96-
content
97-
</Details>,
98-
)
99-
100-
await expect(findByText('See Details')).rejects.toThrow()
101-
expect(await findByTestId('summary')).toBeInTheDocument()
102-
expect((await findByTestId('summary')).tagName).toBe('SUMMARY')
103-
})
104-
105-
it('Does not add default summary if supplied as different element', async () => {
106-
const {findByTestId, findByText} = render(
107-
<Details data-testid="details">
108-
<summary data-testid="summary">custom summary</summary>
109-
content
110-
</Details>,
111-
)
112-
113-
await expect(findByText('See Details')).rejects.toThrow()
114-
expect(await findByTestId('summary')).toBeInTheDocument()
115-
expect((await findByTestId('summary')).tagName).toBe('SUMMARY')
116-
})
117-
11885
describe('Details.Summary', () => {
11986
it('should support a custom `className` on the container element', () => {
12087
render(<Details.Summary className="custom-class">test summary</Details.Summary>)

0 commit comments

Comments
 (0)