-
-
Notifications
You must be signed in to change notification settings - Fork 4.7k
feat(project-creation): Add reusable collapsible section for create flow #117847
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+158
−0
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
55 changes: 55 additions & 0 deletions
55
static/app/views/onboarding/components/scmCollapsibleSection.spec.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary'; | ||
|
|
||
| import {ScmCollapsibleSection} from './scmCollapsibleSection'; | ||
|
|
||
| describe('ScmCollapsibleSection', () => { | ||
| it('renders the title and content expanded by default', () => { | ||
| render( | ||
| <ScmCollapsibleSection title="Section title"> | ||
| <div>Body content</div> | ||
| </ScmCollapsibleSection> | ||
| ); | ||
|
|
||
| expect(screen.getByRole('button', {name: 'Section title'})).toBeInTheDocument(); | ||
| expect(screen.getByText('Body content')).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('starts collapsed when defaultExpanded is false', () => { | ||
| render( | ||
| <ScmCollapsibleSection title="Section title" defaultExpanded={false}> | ||
| <div>Body content</div> | ||
| </ScmCollapsibleSection> | ||
| ); | ||
|
|
||
| expect(screen.queryByText('Body content')).not.toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('toggles the content when the title is clicked', async () => { | ||
| render( | ||
| <ScmCollapsibleSection title="Section title"> | ||
| <div>Body content</div> | ||
| </ScmCollapsibleSection> | ||
| ); | ||
|
|
||
| const toggle = screen.getByRole('button', {name: 'Section title'}); | ||
| expect(toggle).toHaveAttribute('aria-expanded', 'true'); | ||
|
|
||
| await userEvent.click(toggle); | ||
| expect(toggle).toHaveAttribute('aria-expanded', 'false'); | ||
| expect(screen.queryByText('Body content')).not.toBeInTheDocument(); | ||
|
|
||
| await userEvent.click(toggle); | ||
| expect(toggle).toHaveAttribute('aria-expanded', 'true'); | ||
| expect(screen.getByText('Body content')).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('renders trailing content in the header', () => { | ||
| render( | ||
| <ScmCollapsibleSection title="Section title" trailing={<span>Trailing</span>}> | ||
| <div>Body content</div> | ||
| </ScmCollapsibleSection> | ||
| ); | ||
|
|
||
| expect(screen.getByText('Trailing')).toBeInTheDocument(); | ||
| }); | ||
| }); |
101 changes: 101 additions & 0 deletions
101
static/app/views/onboarding/components/scmCollapsibleSection.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,101 @@ | ||
| import {useId, useState} from 'react'; | ||
| import styled from '@emotion/styled'; | ||
| import {AnimatePresence, motion} from 'framer-motion'; | ||
|
|
||
| import {Button} from '@sentry/scraps/button'; | ||
| import {Flex, Stack} from '@sentry/scraps/layout'; | ||
| import {Text} from '@sentry/scraps/text'; | ||
|
|
||
| import {IconChevron} from 'sentry/icons'; | ||
|
|
||
| interface ScmCollapsibleSectionProps { | ||
| children: React.ReactNode; | ||
| title: React.ReactNode; | ||
| /** | ||
| * Whether the section starts expanded. Defaults to true. | ||
| */ | ||
| defaultExpanded?: boolean; | ||
| /** | ||
| * Rendered at the far right of the title row (e.g. a helper label). Stays in | ||
| * the header whether or not the section is expanded. | ||
| */ | ||
| trailing?: React.ReactNode; | ||
| } | ||
|
|
||
| /** | ||
| * A collapsible section for the SCM project-creation flow: a chevron and the | ||
| * title share one transparent toggle button (mirroring the core Disclosure | ||
| * look) with an optional trailing slot pinned right, and the body animates its | ||
| * own height so sibling cards in a framer-motion `layout="position"` group | ||
| * follow via normal document flow. `initial={false}` keeps it from animating on | ||
| * mount, so it renders in its `defaultExpanded` state. | ||
| * | ||
| * This is a local variant of the core Disclosure rather than a consumer of it: | ||
| * Disclosure.Content hides with `display: none`, which can't tween and won't | ||
| * reflow sibling cards, and Disclosure.Title's full-width stretched button | ||
| * can't express a content-hugging toggle without forking the shared component. | ||
| */ | ||
| export function ScmCollapsibleSection({ | ||
| title, | ||
| trailing, | ||
| defaultExpanded = true, | ||
| children, | ||
| }: ScmCollapsibleSectionProps) { | ||
| const [expanded, setExpanded] = useState(defaultExpanded); | ||
| const contentId = useId(); | ||
|
|
||
| return ( | ||
| <Stack gap="0" width="100%"> | ||
| <Flex justify="between" align="center" width="100%"> | ||
| <ToggleButton | ||
| variant="transparent" | ||
| size="md" | ||
| icon={<IconChevron direction={expanded ? 'down' : 'right'} />} | ||
| aria-expanded={expanded} | ||
| // Only reference the content while it is in the DOM: the body is | ||
| // conditionally rendered, so a static aria-controls would point at a | ||
| // missing IDREF when collapsed. | ||
| aria-controls={expanded ? contentId : undefined} | ||
| onClick={() => setExpanded(value => !value)} | ||
| > | ||
| <Text as="span" bold> | ||
| {title} | ||
| </Text> | ||
| </ToggleButton> | ||
| {trailing} | ||
| </Flex> | ||
| <AnimatePresence initial={false}> | ||
| {expanded && ( | ||
| <motion.div | ||
| id={contentId} | ||
| key="content" | ||
| initial={{height: 0, opacity: 0}} | ||
| animate={{height: 'auto', opacity: 1}} | ||
| exit={{height: 0, opacity: 0}} | ||
| transition={{duration: 0.2, ease: 'easeOut'}} | ||
| style={{overflow: 'hidden', width: '100%'}} | ||
| > | ||
| <Content width="100%">{children}</Content> | ||
| </motion.div> | ||
| )} | ||
| </AnimatePresence> | ||
| </Stack> | ||
| ); | ||
| } | ||
|
|
||
| // Mirrors core Disclosure's StretchedButton: a transparent toggle holding the | ||
| // chevron + title that hugs its content, with the left padding pulled in so the | ||
| // chevron sits near-flush with the section edge. | ||
| const ToggleButton = styled(Button)` | ||
| padding-left: ${p => p.theme.space.xs}; | ||
| `; | ||
|
|
||
| // Indents the body so its left edge lines up with the title copy inside | ||
| // ToggleButton: button padding-left (xs) + chevron width (md button -> sm icon, | ||
| // 14px) + the button's icon gap (md). Matches core Disclosure's 26px inset. | ||
| // padding-top lives here (not as a Stack gap) so the spacing collapses with the | ||
| // height animation instead of leaving a gap behind the title. | ||
| const Content = styled(Stack)` | ||
| padding-top: ${p => p.theme.space.md}; | ||
| padding-left: calc(${p => p.theme.space.xs} + 14px + ${p => p.theme.space.md}); | ||
| `; | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.