Skip to content

Commit 62cafc5

Browse files
laraharrowLuke Bowerman
andauthored
feat: Panel component (#1815)
* firt commit * Panel working needs style * clean up and added documentation * documentation added to Panel's component * update tests * firt commit * updating component to support HeaderPanel * working on hooks example * update to test hooks * update Panel to use List instead of Menu * Small tweaks * updated documentation to include peculiarity of hooks * updated names on storybook examples * update Panel to support all directions * update documantation * updating documentation * update tests for codecov * update test for better coverge clean up directin * update PanelHeader based on design and better test coverage * updates after final review * Name & import PanelDirection consistently * Correct CHANGELOG * Correct circular dependencies * Add snapshots Co-authored-by: Luke Bowerman <[email protected]>
1 parent e5bfbfc commit 62cafc5

File tree

14 files changed

+910
-1
lines changed

14 files changed

+910
-1
lines changed
5.72 KB
Loading
5.73 KB
Loading
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/*
2+
3+
MIT License
4+
5+
Copyright (c) 2020 Looker Data Sciences, Inc.
6+
7+
Permission is hereby granted, free of charge, to any person obtaining a copy
8+
of this software and associated documentation files (the "Software"), to deal
9+
in the Software without restriction, including without limitation the rights
10+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11+
copies of the Software, and to permit persons to whom the Software is
12+
furnished to do so, subject to the following conditions:
13+
14+
The above copyright notice and this permission notice shall be included in all
15+
copies or substantial portions of the Software.
16+
17+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23+
SOFTWARE.
24+
25+
*/
26+
27+
import React from 'react'
28+
import { Story } from '@storybook/react/types-6-0'
29+
import { List, ListItem } from '../List'
30+
import { Aside, Page, Section } from '../Layout'
31+
import { Panel, Panels, PanelProps, usePanel } from './'
32+
33+
export default {
34+
component: Panel,
35+
title: 'Panel',
36+
}
37+
38+
const Template: Story<PanelProps> = (args) => (
39+
<Page hasAside>
40+
<Aside width="12rem">
41+
<Panels>
42+
<List>
43+
<Panel {...args}>
44+
<ListItem>option A</ListItem>
45+
</Panel>
46+
<ListItem>option B</ListItem>
47+
<ListItem>option C</ListItem>
48+
<ListItem>option D</ListItem>
49+
</List>
50+
</Panels>
51+
</Aside>
52+
<Section>Main stuff here...</Section>
53+
</Page>
54+
)
55+
56+
export const Basic = Template.bind({})
57+
Basic.args = {
58+
content: 'Panel Content',
59+
title: 'Panel Title',
60+
}
61+
Basic.parameters = {
62+
storyshots: { disable: true },
63+
}
64+
65+
export const Open = Template.bind({})
66+
Open.args = {
67+
...Basic.args,
68+
defaultOpen: true,
69+
}
70+
71+
export const DirectionRight = Template.bind({})
72+
DirectionRight.args = {
73+
...Basic.args,
74+
defaultOpen: true,
75+
direction: 'right',
76+
}
77+
78+
export const Hook = () => {
79+
const { panel, setOpen } = usePanel({
80+
content: 'Panel content',
81+
title: 'Panel Hook',
82+
})
83+
84+
return (
85+
<>
86+
<List>
87+
<ListItem onClick={() => setOpen(true)} icon="Check">
88+
Option A
89+
</ListItem>
90+
<ListItem icon="Check">Option B</ListItem>
91+
</List>
92+
{panel}
93+
</>
94+
)
95+
}
96+
Hook.parameters = {
97+
storyshots: { disable: true },
98+
}
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
/*
2+
3+
MIT License
4+
5+
Copyright (c) 2020 Looker Data Sciences, Inc.
6+
7+
Permission is hereby granted, free of charge, to any person obtaining a copy
8+
of this software and associated documentation files (the "Software"), to deal
9+
in the Software without restriction, including without limitation the rights
10+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11+
copies of the Software, and to permit persons to whom the Software is
12+
furnished to do so, subject to the following conditions:
13+
14+
The above copyright notice and this permission notice shall be included in all
15+
copies or substantial portions of the Software.
16+
17+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23+
SOFTWARE.
24+
25+
*/
26+
27+
import '@testing-library/jest-dom/extend-expect'
28+
import { fireEvent } from '@testing-library/react'
29+
import 'jest-styled-components'
30+
import React, { useState } from 'react'
31+
import { renderWithTheme } from '@looker/components-test-utils'
32+
import { Panel, Panels, usePanel } from './'
33+
34+
const globalConsole = global.console
35+
/* eslint-disable-next-line @typescript-eslint/unbound-method */
36+
const globalGetBoundingClientRect = Element.prototype.getBoundingClientRect
37+
38+
beforeEach(() => {
39+
global.console = {
40+
...globalConsole,
41+
error: jest.fn(),
42+
warn: jest.fn(),
43+
}
44+
/* eslint-disable-next-line @typescript-eslint/unbound-method */
45+
Element.prototype.getBoundingClientRect = jest.fn(() => {
46+
return {
47+
bottom: 0,
48+
height: 30,
49+
left: 0,
50+
right: 0,
51+
toJSON: jest.fn(),
52+
top: 0,
53+
width: 360,
54+
x: 0,
55+
y: 0,
56+
}
57+
})
58+
})
59+
60+
afterEach(() => {
61+
jest.resetAllMocks()
62+
global.console = globalConsole
63+
/* eslint-disable-next-line @typescript-eslint/unbound-method */
64+
Element.prototype.getBoundingClientRect = globalGetBoundingClientRect
65+
})
66+
67+
describe('Panel', () => {
68+
const UsePanelHook = () => {
69+
const [isOpen, setOpen] = useState(false)
70+
const open = () => setOpen(true)
71+
const canClose = () => true
72+
73+
const { panel } = usePanel({
74+
canClose,
75+
content: 'Panel content',
76+
direction: 'left',
77+
isOpen,
78+
setOpen,
79+
title: 'Panel Hook',
80+
})
81+
82+
return (
83+
<>
84+
{panel}
85+
<ul>
86+
<li onClick={open}>Option A</li>
87+
<li>Option B</li>
88+
</ul>
89+
</>
90+
)
91+
}
92+
93+
const ControlledPanel = () => {
94+
const [option, setOption] = useState(false)
95+
const toggleOption = () => setOption(!option)
96+
97+
return (
98+
<Panels>
99+
<ul>
100+
<li onClick={toggleOption}>Option 1</li>
101+
<li>Option 2</li>
102+
<li>Option 3</li>
103+
</ul>
104+
<Panel
105+
isOpen={option}
106+
setOpen={setOption}
107+
direction="left"
108+
title="return to main option"
109+
content={
110+
<ul>
111+
<li>Panel 1</li>
112+
<li>Panel 2</li>
113+
<li>Panel 3</li>
114+
</ul>
115+
}
116+
/>
117+
</Panels>
118+
)
119+
}
120+
121+
const UncontrolledPanel = () => (
122+
<Panels>
123+
<ul>
124+
<Panel
125+
content={'content from the right edge...'}
126+
direction="right"
127+
title="Right"
128+
>
129+
<li>Right</li>
130+
</Panel>
131+
<Panel title="Left" content={'content from the left edge...'}>
132+
<li>Left</li>
133+
</Panel>
134+
<Panel content="My neat dialog" title="render prop">
135+
{(panelProps) => <li {...panelProps}>render prop</li>}
136+
</Panel>
137+
</ul>
138+
</Panels>
139+
)
140+
141+
test('Panel works properly when direction is equal to right', () => {
142+
const { getByText } = renderWithTheme(<UncontrolledPanel />)
143+
144+
const right = getByText('Right')
145+
expect(right).toBeInTheDocument()
146+
147+
fireEvent.click(right)
148+
expect(getByText('content from the right edge...')).toBeInTheDocument()
149+
})
150+
151+
test('uncontrolled Panel displays content prop', () => {
152+
const { getByText } = renderWithTheme(<UncontrolledPanel />)
153+
154+
const left = getByText('Left')
155+
expect(left).toBeInTheDocument()
156+
157+
fireEvent.click(left)
158+
159+
// Find content
160+
expect(getByText('content from the left edge...')).toBeInTheDocument()
161+
})
162+
163+
test('controlled Panel displays content', () => {
164+
const { getByText } = renderWithTheme(<ControlledPanel />)
165+
166+
const liElement = getByText('Option 1')
167+
expect(liElement).toBeInTheDocument()
168+
169+
fireEvent.click(liElement)
170+
171+
// Find content
172+
expect(getByText('Panel 1')).toBeInTheDocument()
173+
expect(getByText('Panel 3')).toBeInTheDocument()
174+
})
175+
176+
test('returns to previous display if title prop is clicked', () => {
177+
const { getByText } = renderWithTheme(<ControlledPanel />)
178+
179+
const liElement = getByText('Option 1')
180+
expect(liElement).toBeInTheDocument()
181+
182+
fireEvent.click(liElement)
183+
184+
// Find content
185+
expect(getByText('Panel 1')).toBeInTheDocument()
186+
187+
const returnToExplore = getByText('return to main option')
188+
189+
fireEvent.click(returnToExplore)
190+
191+
expect(getByText('Option 1')).toBeInTheDocument()
192+
expect(getByText('Option 2')).toBeInTheDocument()
193+
})
194+
195+
test('usePanel hook works as expected', () => {
196+
const { getByText } = renderWithTheme(<UsePanelHook />)
197+
198+
expect(getByText('Option A')).toBeInTheDocument()
199+
expect(getByText('Option B')).toBeInTheDocument()
200+
201+
fireEvent.click(getByText('Option A'))
202+
203+
expect(getByText('Panel content')).toBeInTheDocument()
204+
205+
fireEvent.click(getByText('Close Panel Hook'))
206+
207+
expect(getByText('Option A')).toBeInTheDocument()
208+
})
209+
210+
test('with no content fails', () => {
211+
const { getByText } = renderWithTheme(<UncontrolledPanel />)
212+
expect(getByText('render prop')).toBeInTheDocument()
213+
fireEvent.click(getByText('render prop'))
214+
expect(getByText('My neat dialog')).toBeInTheDocument()
215+
})
216+
217+
test('triggers console.warn if no children is passed', () => {
218+
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
219+
// @ts-ignore
220+
renderWithTheme(<Panel>{null}</Panel>)
221+
// eslint-disable-next-line no-console
222+
expect(console.warn).toHaveBeenCalled()
223+
})
224+
})
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
3+
MIT License
4+
5+
Copyright (c) 2020 Looker Data Sciences, Inc.
6+
7+
Permission is hereby granted, free of charge, to any person obtaining a copy
8+
of this software and associated documentation files (the "Software"), to deal
9+
in the Software without restriction, including without limitation the rights
10+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11+
copies of the Software, and to permit persons to whom the Software is
12+
furnished to do so, subject to the following conditions:
13+
14+
The above copyright notice and this permission notice shall be included in all
15+
copies or substantial portions of the Software.
16+
17+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23+
SOFTWARE.
24+
25+
*/
26+
27+
import React, { FC, ReactNode, isValidElement, cloneElement } from 'react'
28+
import { PanelProps, PanelRenderProp } from './types'
29+
import { usePanel } from './usePanel'
30+
31+
const isRenderProp = (
32+
children: ReactNode | PanelRenderProp
33+
): children is PanelRenderProp => typeof children === 'function'
34+
35+
export const Panel: FC<PanelProps> = ({ children, content, ...props }) => {
36+
const { domProps, panel } = usePanel({ content, ...props })
37+
38+
if (children === undefined) {
39+
return <>{panel}</>
40+
} else if (isValidElement(children)) {
41+
children = cloneElement(children, {
42+
...domProps,
43+
})
44+
} else if (isRenderProp(children)) {
45+
children = children(domProps)
46+
} else {
47+
// eslint-disable-next-line no-console
48+
console.warn(
49+
`Element "${typeof children}" can't be used as target for Panel`
50+
)
51+
}
52+
53+
return (
54+
<>
55+
{children}
56+
{panel}
57+
</>
58+
)
59+
}

0 commit comments

Comments
 (0)