Skip to content

Commit cca78a1

Browse files
authored
Allow Accordion component to be either controlled or uncontrolled (#940)
* refactor Accordion * remove handleOpen usage * allow colorMode to be set on a per-story basis * add more Accordion stories and examples * update snapshots * remove unnecessary line from story controls * update snapshots after rebase * revert removed comments * update toggleColor initial value in storybook * use event listeners to bind Accordion callbacks * add changeset * fix storybook 'all' color mode height bug
1 parent 5f31936 commit cca78a1

12 files changed

+387
-276
lines changed

.changeset/two-keys-arrive.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@primer/react-brand': patch
3+
---
4+
5+
Updated the `Accordion` component to support additional state handling methods. All changes are backwards compatible. Refer to Storybook for examples of these additional `Accordion` features.

apps/storybook/.storybook/preview.js

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,16 +36,17 @@ export const globalTypes = {
3636
}
3737

3838
const ThemeProviderDecorator = (Story, context) => {
39+
const colorMode = context.parameters?.colorMode || context.globals.colorMode
3940
// from Storybook v7, this can't be applied as a side effect
40-
if (context.globals.colorMode === 'auto') {
41+
if (colorMode === 'auto') {
4142
document.body.removeAttribute('data-color-mode')
4243
}
4344

44-
if (['light', 'dark'].includes(context.globals.colorMode)) {
45-
document.body.setAttribute('data-color-mode', context.globals.colorMode)
45+
if (['light', 'dark'].includes(colorMode)) {
46+
document.body.setAttribute('data-color-mode', colorMode)
4647
}
4748

48-
if (context && context.globals.colorMode === 'all') {
49+
if (context && colorMode === 'all') {
4950
return (
5051
<div className={styles['color-mode-all']}>
5152
<style
@@ -64,7 +65,7 @@ const ThemeProviderDecorator = (Story, context) => {
6465
}
6566

6667
return (
67-
<ThemeProvider colorMode={context.globals.colorMode}>
68+
<ThemeProvider colorMode={colorMode}>
6869
<Story {...context} />
6970
</ThemeProvider>
7071
)

apps/storybook/.storybook/preview.module.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
.color-mode-all {
22
display: grid;
33
grid-template-columns: 1fr 1fr;
4-
grid-template-rows: 100vh;
4+
grid-template-rows: 100%;
55
}
66

77
.color-mode-all > [data-color-mode] {
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
import React, {useCallback, useRef} from 'react'
2+
import type {Meta, StoryObj} from '@storybook/react'
3+
import {Accordion, AccordionRootProps, AccordionToggleColors} from '.'
4+
import {Box, Stack, UnorderedList} from '../'
5+
6+
const meta: Meta = {
7+
title: 'Components/Accordion/Features',
8+
component: Accordion,
9+
}
10+
export default meta
11+
12+
export const Composition: StoryObj = {
13+
render: () => (
14+
<>
15+
<Accordion>
16+
<Accordion.Heading>What&apos;s included in the GitHub for Startups offer?</Accordion.Heading>
17+
<Accordion.Content>
18+
<p>
19+
All GitHub for Startups companies receive up to 20 seats of GitHub Enterprise for free for year one and 50%
20+
off year two. Learn more about the features and capabilities of GitHub Enterprise{' '}
21+
<a href="https://copilot.github.com/" target="_blank" rel="noreferrer">
22+
here
23+
</a>
24+
.
25+
</p>
26+
</Accordion.Content>
27+
</Accordion>
28+
<Accordion>
29+
<Accordion.Heading>Who is eligible to apply?</Accordion.Heading>
30+
<Accordion.Content>
31+
<p>Startups who meet the following criteria are eligible to apply for the program:</p>
32+
<UnorderedList>
33+
<UnorderedList.Item>Must be associated with a current GitHub for Startups partner.</UnorderedList.Item>
34+
<UnorderedList.Item>Self-funded or funded (Seed-Series A)</UnorderedList.Item>
35+
<UnorderedList.Item>Not a current GitHub Enterprise customer</UnorderedList.Item>
36+
<UnorderedList.Item>Must not have previously received credits for GitHub Enterprise</UnorderedList.Item>
37+
</UnorderedList>
38+
</Accordion.Content>
39+
</Accordion>
40+
<Accordion>
41+
<Accordion.Heading>What if my startup is not eligible? Are there other resources for me?</Accordion.Heading>
42+
<Accordion.Content>
43+
<p>
44+
If you’re not currently eligible for the GitHub for Startups but would like to try GitHub Enterprise, please
45+
feel to sign up for a trial{' '}
46+
<a href="https://copilot.github.com/" target="_blank" rel="noreferrer">
47+
here
48+
</a>
49+
.
50+
</p>
51+
</Accordion.Content>
52+
</Accordion>
53+
<Accordion>
54+
<Accordion.Heading>How can my organization become a GitHub for Startups partner?</Accordion.Heading>
55+
<Accordion.Content>
56+
<p>
57+
Any investor, accelerator, or startup support organization is eligible to apply for the GitHub for Startups
58+
program.
59+
</p>
60+
<p>
61+
{' '}
62+
<a href="https://copilot.github.com/" target="_blank" rel="noreferrer">
63+
Apply here
64+
</a>
65+
.
66+
</p>
67+
</Accordion.Content>
68+
</Accordion>
69+
</>
70+
),
71+
}
72+
73+
export const ToggleColors: StoryObj = {
74+
parameters: {
75+
colorMode: 'all',
76+
},
77+
render: () => {
78+
return (
79+
<Stack direction="vertical">
80+
<Box backgroundColor="default" padding="condensed">
81+
{AccordionToggleColors.map(color => (
82+
<Accordion key={color}>
83+
<Accordion.Heading toggleColor={color}>Toggle color: {color}</Accordion.Heading>
84+
<Accordion.Content>
85+
<p>Description</p>
86+
</Accordion.Content>
87+
</Accordion>
88+
))}
89+
</Box>
90+
</Stack>
91+
)
92+
},
93+
}
94+
95+
const headings = [
96+
"What's included in the GitHub for Startups offer?",
97+
'Who is eligible to apply?',
98+
'What if my startup is not eligible? Are there other resources for me?',
99+
'How can my organization become a GitHub for Startups partner?',
100+
]
101+
102+
export const AlwaysExactlyOnePanelOpen: StoryObj = {
103+
render: () => {
104+
// eslint-disable-next-line react-hooks/rules-of-hooks
105+
const containerRef = useRef<HTMLDivElement>(null)
106+
107+
// eslint-disable-next-line react-hooks/rules-of-hooks
108+
const onClick = useCallback<NonNullable<AccordionRootProps['onClick']>>(e => {
109+
// Prevent closing of an open accordion
110+
if (e.currentTarget.open) {
111+
e.preventDefault()
112+
return
113+
}
114+
115+
if (!containerRef.current) return
116+
117+
// Close all other accordions
118+
const openAccordions = containerRef.current.querySelectorAll('details[open]')
119+
for (const openAccordion of openAccordions) {
120+
if (openAccordion !== e.currentTarget) {
121+
openAccordion.removeAttribute('open')
122+
}
123+
}
124+
}, [])
125+
126+
// eslint-disable-next-line react-hooks/rules-of-hooks
127+
const onKeyDownCapture = useCallback<NonNullable<AccordionRootProps['onKeyDownCapture']>>(e => {
128+
// Prevent the escape key from closing the accordion
129+
if (e.key === 'Escape') {
130+
e.preventDefault()
131+
e.stopPropagation()
132+
}
133+
}, [])
134+
135+
return (
136+
<div ref={containerRef}>
137+
{headings.map((heading, index) => (
138+
<Accordion key={index} open={index === 0} onClick={onClick} onKeyDownCapture={onKeyDownCapture}>
139+
<Accordion.Heading>{heading}</Accordion.Heading>
140+
<Accordion.Content>
141+
<p>
142+
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Blanditiis quidem veniam vero omnis
143+
consequuntur cum quae libero dolor dicta odio in, corporis perspiciatis nesciunt facere. Eius vero culpa
144+
quae itaque?
145+
</p>
146+
</Accordion.Content>
147+
</Accordion>
148+
))}
149+
</div>
150+
)
151+
},
152+
}
153+
154+
export const ExclusiveUsingNameAttribute: StoryObj = {
155+
render: () => (
156+
<>
157+
{headings.map((heading, index) => (
158+
<Accordion key={index} name="exclusive">
159+
<Accordion.Heading>{heading}</Accordion.Heading>
160+
<Accordion.Content>
161+
<p>
162+
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Blanditiis quidem veniam vero omnis consequuntur
163+
cum quae libero dolor dicta odio in, corporis perspiciatis nesciunt facere. Eius vero culpa quae itaque?
164+
</p>
165+
</Accordion.Content>
166+
</Accordion>
167+
))}
168+
</>
169+
),
170+
}
171+
172+
export const ExclusiveWithoutUsingNameAttribute: StoryObj = {
173+
render: () => {
174+
// eslint-disable-next-line react-hooks/rules-of-hooks
175+
const containerRef = useRef<HTMLDivElement>(null)
176+
177+
// eslint-disable-next-line react-hooks/rules-of-hooks
178+
const onClick = useCallback<NonNullable<AccordionRootProps['onClick']>>(e => {
179+
if (!containerRef.current) return
180+
181+
// Close all other accordions
182+
const openAccordions = containerRef.current.querySelectorAll('details[open]')
183+
for (const openAccordion of openAccordions) {
184+
if (openAccordion !== e.currentTarget) {
185+
openAccordion.removeAttribute('open')
186+
}
187+
}
188+
}, [])
189+
190+
return (
191+
<div ref={containerRef}>
192+
{headings.map((heading, index) => (
193+
<Accordion key={index} onClick={onClick}>
194+
<Accordion.Heading>{heading}</Accordion.Heading>
195+
<Accordion.Content>
196+
<p>
197+
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Blanditiis quidem veniam vero omnis
198+
consequuntur cum quae libero dolor dicta odio in, corporis perspiciatis nesciunt facere. Eius vero culpa
199+
quae itaque?
200+
</p>
201+
</Accordion.Content>
202+
</Accordion>
203+
))}
204+
</div>
205+
)
206+
},
207+
}

0 commit comments

Comments
 (0)