Skip to content

Commit 756ff15

Browse files
author
Elliot
authored
Fieldset: Accordion Mode (#1018)
* Initial Commit * Added accordion prop to Fieldset * Reworked Accordion interfaces - Made Accordion interfaces dry-er so we can extend off smaller pieces in Fieldset's interface * Implemented accordion props onto internal Acccordion in Fieldset * Updated Fieldset test suite * Added documentation on accordion mode * Added note on using accordion prop without legend value * CHANGELOG update * Updated snapshot * Updated Fieldset accordion style * Updated Fieldset accordion style * Updated snapshot * Luke Feedback
1 parent c98db8e commit 756ff15

File tree

9 files changed

+412
-149
lines changed

9 files changed

+412
-149
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [UNRELEASED]
99

10+
### Added
11+
12+
- `Fieldset` supports an accordion mode via an `accordion` prop
13+
1014
### Changed
1115

1216
- `SpaceVertical` now has `align=stretch` by default

packages/components/src/Accordion/Accordion.tsx

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -63,12 +63,7 @@ export interface AccordionIndicatorProps {
6363
indicatorIcons?: IndicatorIcons
6464
}
6565

66-
export interface AccordionProps
67-
extends AccordionIndicatorProps,
68-
SimpleLayoutProps {
69-
children: ReactNode
70-
className?: string
71-
66+
export interface AccordionControlProps {
7267
/**
7368
* Use this property if you wish to use the component in a `uncontrolled` manner and have it open when initially rendering.
7469
* Component will hold internal state and open and close on disclosure click
@@ -95,6 +90,31 @@ export interface AccordionProps
9590
onOpen?: () => void // called when the component is opened
9691
}
9792

93+
/**
94+
* Keys below are used by Fieldset to omit Accordion related props so they can be spread onto the internal Accordion component
95+
*/
96+
export const AccordionIndicatorPropKeys = [
97+
'indicatorPosition',
98+
'indicatorSize',
99+
'indicatorGap',
100+
'indicatorIcons',
101+
]
102+
export const AccordionControlPropKeys = [
103+
'defaultOpen',
104+
'isOpen',
105+
'toggleOpen',
106+
'onClose',
107+
'onOpen',
108+
]
109+
110+
export interface AccordionProps
111+
extends AccordionControlProps,
112+
AccordionIndicatorProps,
113+
SimpleLayoutProps {
114+
children: ReactNode
115+
className?: string
116+
}
117+
98118
const AccordionLayout: FC<AccordionProps> = ({
99119
children,
100120
className,

packages/components/src/Form/Fieldset/Fieldset.test.tsx

Lines changed: 85 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,17 +25,93 @@
2525
*/
2626

2727
import 'jest-styled-components'
28-
import React from 'react'
29-
import { assertSnapshot } from '@looker/components-test-utils'
28+
import React, { useState } from 'react'
29+
import { assertSnapshot, renderWithTheme } from '@looker/components-test-utils'
30+
import { fireEvent } from '@testing-library/react'
3031
import { FieldText } from '../Fields/FieldText'
3132
import { Fieldset } from './Fieldset'
3233

34+
const fieldTexts = (
35+
<>
36+
<FieldText label="one" name="name1" id="text-1" />
37+
<FieldText label="two" name="name2" id="text-2" />
38+
<FieldText label="three" name="nam3" id="text-3" />
39+
</>
40+
)
41+
3342
test('Fieldset', () => {
34-
assertSnapshot(
35-
<Fieldset legend="Legend">
36-
<FieldText label="One" name="name1" id="text-1" />
37-
<FieldText label="two" name="name2" id="text-2" />
38-
<FieldText label="three" name="nam3" id="text-3" />
39-
</Fieldset>
40-
)
43+
assertSnapshot(<Fieldset legend="Legend">{fieldTexts}</Fieldset>)
44+
})
45+
46+
describe('Fieldset - Accordion mode', () => {
47+
test('Renders legend and children (on legend click)', () => {
48+
const { getByText, queryByText } = renderWithTheme(
49+
<Fieldset legend="Legend" accordion>
50+
{fieldTexts}
51+
</Fieldset>
52+
)
53+
54+
expect(queryByText('one')).not.toBeInTheDocument()
55+
expect(queryByText('two')).not.toBeInTheDocument()
56+
expect(queryByText('three')).not.toBeInTheDocument()
57+
fireEvent.click(getByText('Legend'))
58+
getByText('one')
59+
getByText('two')
60+
getByText('three')
61+
})
62+
63+
test('Renders children by default when defaultOpen === true', () => {
64+
const { getByText } = renderWithTheme(
65+
<Fieldset legend="Legend" accordion defaultOpen>
66+
{fieldTexts}
67+
</Fieldset>
68+
)
69+
70+
getByText('one')
71+
getByText('two')
72+
getByText('three')
73+
})
74+
75+
test('Triggers onClose and onOpen callbacks on legend click', () => {
76+
const onClose = jest.fn()
77+
const onOpen = jest.fn()
78+
79+
const { getByText } = renderWithTheme(
80+
<Fieldset legend="Legend" accordion onClose={onClose} onOpen={onOpen}>
81+
{fieldTexts}
82+
</Fieldset>
83+
)
84+
85+
const disclosure = getByText('Legend')
86+
fireEvent.click(disclosure)
87+
expect(onOpen).toHaveBeenCalled()
88+
fireEvent.click(disclosure)
89+
expect(onClose).toHaveBeenCalled()
90+
})
91+
92+
test('Shows and hides children on legend click with provided isOpen and toggleOpen props', () => {
93+
const Wrapper = () => {
94+
const [isOpen, setIsOpen] = useState(false)
95+
return (
96+
<Fieldset
97+
legend="Legend"
98+
accordion
99+
isOpen={isOpen}
100+
toggleOpen={setIsOpen}
101+
>
102+
{fieldTexts}
103+
</Fieldset>
104+
)
105+
}
106+
107+
const { getByText, queryByText } = renderWithTheme(<Wrapper />)
108+
109+
expect(queryByText('one')).not.toBeInTheDocument()
110+
expect(queryByText('two')).not.toBeInTheDocument()
111+
expect(queryByText('three')).not.toBeInTheDocument()
112+
fireEvent.click(getByText('Legend'))
113+
getByText('one')
114+
getByText('two')
115+
getByText('three')
116+
})
41117
})

packages/components/src/Form/Fieldset/Fieldset.tsx

Lines changed: 74 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -27,25 +27,61 @@
2727
import React, { forwardRef, ReactNode, Ref } from 'react'
2828
import styled from 'styled-components'
2929
import { CompatibleHTMLProps } from '@looker/design-tokens'
30+
import omit from 'lodash/omit'
31+
import pick from 'lodash/pick'
3032
import { Space, SpaceHelperProps, SpaceVertical } from '../../Layout'
3133
import { Legend } from '../Legend'
34+
import {
35+
Accordion,
36+
AccordionContent,
37+
AccordionControlProps,
38+
AccordionControlPropKeys,
39+
AccordionDisclosure,
40+
AccordionIndicatorProps,
41+
} from '../../Accordion'
3242

3343
export interface FieldsetProps
3444
extends SpaceHelperProps,
35-
CompatibleHTMLProps<HTMLDivElement> {
45+
CompatibleHTMLProps<HTMLDivElement>,
46+
AccordionControlProps {
47+
/** If true, the Fieldset will be wrapped by an Accordion structure (i.e. a collapsible section)
48+
* @default false
49+
*/
50+
accordion?: boolean
51+
ariaLabeledby?: string
3652
/** Determines where to place the label in relation to the input.
3753
* @default false
3854
*/
3955
inline?: boolean
40-
41-
ariaLabeledby?: string
42-
56+
/** Displayed above the children of Fieldset
57+
*/
4358
legend?: ReactNode
4459
}
4560

61+
const accordionIndicatorDefaults: AccordionIndicatorProps = {
62+
indicatorGap: 'xsmall',
63+
indicatorIcons: {
64+
close: 'ArrowRight',
65+
open: 'ArrowDown',
66+
},
67+
indicatorPosition: 'left',
68+
indicatorSize: 'medium',
69+
}
70+
4671
const FieldsetLayout = forwardRef(
4772
(props: FieldsetProps, ref: Ref<HTMLDivElement>) => {
48-
const { inline, className, legend, children, ...restProps } = props
73+
const {
74+
accordion,
75+
inline,
76+
className,
77+
legend,
78+
children,
79+
...restProps
80+
} = omit(props, [...AccordionControlPropKeys])
81+
const accordionProps = {
82+
...pick(props, [...AccordionControlPropKeys]),
83+
...accordionIndicatorDefaults,
84+
}
4985
const LayoutComponent = inline ? Space : SpaceVertical
5086

5187
/**
@@ -61,7 +97,6 @@ const FieldsetLayout = forwardRef(
6197
<LayoutComponent
6298
{...restProps}
6399
gap={inline ? 'medium' : 'small'}
64-
className={className}
65100
ref={ref}
66101
role="group"
67102
align="start"
@@ -70,20 +105,47 @@ const FieldsetLayout = forwardRef(
70105
</LayoutComponent>
71106
)
72107

73-
return legend ? (
74-
<SpaceVertical>
75-
{typeof legend === 'string' ? <Legend>{legend}</Legend> : legend}
76-
{content}
77-
</SpaceVertical>
108+
!legend &&
109+
accordion &&
110+
// eslint-disable-next-line no-console
111+
console.warn(
112+
'Please provide a value for the "legend" prop if using accordion mode'
113+
)
114+
115+
const renderedFieldset = legend ? (
116+
accordion ? (
117+
<Accordion {...accordionProps}>
118+
<AccordionDisclosure>{legend}</AccordionDisclosure>
119+
<AccordionContent>{content}</AccordionContent>
120+
</Accordion>
121+
) : (
122+
<SpaceVertical>
123+
{typeof legend === 'string' ? <Legend>{legend}</Legend> : legend}
124+
{content}
125+
</SpaceVertical>
126+
)
78127
) : (
79128
content
80129
)
130+
131+
return <div className={className}>{renderedFieldset}</div>
81132
}
82133
)
83134

84135
FieldsetLayout.displayName = 'FieldsetLayout'
85136

86-
export const Fieldset = styled(FieldsetLayout)``
137+
export const Fieldset = styled(FieldsetLayout)`
138+
${AccordionDisclosure} {
139+
font-size: ${({ theme }) => theme.fontSizes.small};
140+
font-weight: ${({ theme }) => theme.fontWeights.semiBold};
141+
height: 24px;
142+
padding: ${({ theme }) => `${theme.space.xxsmall} 0`};
143+
}
144+
145+
${AccordionContent} {
146+
padding-top: ${({ theme }) => theme.space.medium};
147+
}
148+
`
87149

88150
Fieldset.defaultProps = {
89151
padding: 'none',

0 commit comments

Comments
 (0)