@sproutsocial/seeds-react-panel 0.3.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.js +4 -0
- package/.turbo/turbo-build.log +21 -0
- package/CHANGELOG.md +139 -0
- package/dist/esm/index.js +366 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/index.d.mts +178 -0
- package/dist/index.d.ts +178 -0
- package/dist/index.js +410 -0
- package/dist/index.js.map +1 -0
- package/jest.config.js +9 -0
- package/package.json +49 -0
- package/src/Panel.stories.tsx +376 -0
- package/src/Panel.tsx +139 -0
- package/src/PanelCloseButton.tsx +26 -0
- package/src/PanelContent.tsx +18 -0
- package/src/PanelContext.tsx +14 -0
- package/src/PanelFooter.tsx +11 -0
- package/src/PanelHeader.tsx +41 -0
- package/src/PanelMobileActionsHeader.tsx +73 -0
- package/src/PanelProvider.tsx +38 -0
- package/src/PanelTypes.ts +166 -0
- package/src/__tests__/Panel.test.tsx +354 -0
- package/src/__tests__/Panel.typetest.tsx +50 -0
- package/src/index.ts +11 -0
- package/src/styles.ts +71 -0
- package/tsconfig.json +15 -0
- package/tsup.config.ts +12 -0
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
3
|
+
import { Box } from "@sproutsocial/seeds-react-box";
|
|
4
|
+
import { Button } from "@sproutsocial/seeds-react-button";
|
|
5
|
+
import { Text } from "@sproutsocial/seeds-react-text";
|
|
6
|
+
import Panel, { PanelProvider, usePanelContext } from "./index";
|
|
7
|
+
|
|
8
|
+
const ToggleButton = ({ label = "Toggle Panel" }: { label?: string }) => {
|
|
9
|
+
const { togglePanel } = usePanelContext();
|
|
10
|
+
return (
|
|
11
|
+
<Button appearance="primary" onClick={togglePanel}>
|
|
12
|
+
{label}
|
|
13
|
+
</Button>
|
|
14
|
+
);
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const LayoutFrame = ({ children }: { children: React.ReactNode }) => (
|
|
18
|
+
<Box display="flex" height="100vh" width="100%" bg="neutral.900">
|
|
19
|
+
{children}
|
|
20
|
+
</Box>
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
const MainContentWrapper = ({ children }: { children: React.ReactNode }) => (
|
|
24
|
+
<Box flex="1 1 auto" overflow="auto" bg="neutral.100">
|
|
25
|
+
{children}
|
|
26
|
+
</Box>
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
const MainContent = () => (
|
|
30
|
+
<MainContentWrapper>
|
|
31
|
+
<Box p={500} mr={100}>
|
|
32
|
+
<Text.Headline as="h1">Main content</Text.Headline>
|
|
33
|
+
<Text.BodyCopy as="p">
|
|
34
|
+
The panel renders inline at desktop widths and pushes this content.
|
|
35
|
+
Below the mobile breakpoint, it switches to a bottom-sheet overlay.
|
|
36
|
+
</Text.BodyCopy>
|
|
37
|
+
<ToggleButton />
|
|
38
|
+
</Box>
|
|
39
|
+
</MainContentWrapper>
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
const PanelBody = () => (
|
|
43
|
+
<Panel.Content>
|
|
44
|
+
<Text.BodyCopy as="p">
|
|
45
|
+
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam ac
|
|
46
|
+
consectetur tortor.
|
|
47
|
+
</Text.BodyCopy>
|
|
48
|
+
</Panel.Content>
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const meta: Meta<typeof Panel> = {
|
|
52
|
+
title: "Components/Panel",
|
|
53
|
+
component: Panel,
|
|
54
|
+
parameters: { layout: "fullscreen" },
|
|
55
|
+
};
|
|
56
|
+
export default meta;
|
|
57
|
+
|
|
58
|
+
type Story = StoryObj<typeof Panel>;
|
|
59
|
+
|
|
60
|
+
export const Default: Story = {
|
|
61
|
+
render: () => (
|
|
62
|
+
<PanelProvider defaultOpen>
|
|
63
|
+
<LayoutFrame>
|
|
64
|
+
<MainContent />
|
|
65
|
+
<Panel
|
|
66
|
+
closeButtonLabel="Close"
|
|
67
|
+
title="Panel title"
|
|
68
|
+
titleId="panel-header"
|
|
69
|
+
aria-labelledby="panel-header"
|
|
70
|
+
>
|
|
71
|
+
<PanelBody />
|
|
72
|
+
</Panel>
|
|
73
|
+
</LayoutFrame>
|
|
74
|
+
</PanelProvider>
|
|
75
|
+
),
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export const LeftSide: Story = {
|
|
79
|
+
render: () => (
|
|
80
|
+
<PanelProvider defaultOpen>
|
|
81
|
+
<LayoutFrame>
|
|
82
|
+
<Panel
|
|
83
|
+
closeButtonLabel="Close"
|
|
84
|
+
direction="left"
|
|
85
|
+
title="Panel title"
|
|
86
|
+
titleId="panel-header"
|
|
87
|
+
aria-labelledby="panel-header"
|
|
88
|
+
>
|
|
89
|
+
<PanelBody />
|
|
90
|
+
</Panel>
|
|
91
|
+
<MainContent />
|
|
92
|
+
</LayoutFrame>
|
|
93
|
+
</PanelProvider>
|
|
94
|
+
),
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export const CustomWidth: Story = {
|
|
98
|
+
render: () => (
|
|
99
|
+
<PanelProvider defaultOpen>
|
|
100
|
+
<LayoutFrame>
|
|
101
|
+
<MainContent />
|
|
102
|
+
<Panel
|
|
103
|
+
closeButtonLabel="Close"
|
|
104
|
+
width={500}
|
|
105
|
+
title="Panel title"
|
|
106
|
+
titleId="panel-header"
|
|
107
|
+
aria-labelledby="panel-header"
|
|
108
|
+
>
|
|
109
|
+
<PanelBody />
|
|
110
|
+
</Panel>
|
|
111
|
+
</LayoutFrame>
|
|
112
|
+
</PanelProvider>
|
|
113
|
+
),
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
export const CustomHeader: Story = {
|
|
117
|
+
render: () => (
|
|
118
|
+
<PanelProvider defaultOpen>
|
|
119
|
+
<LayoutFrame>
|
|
120
|
+
<MainContent />
|
|
121
|
+
<Panel
|
|
122
|
+
closeButtonLabel="Close"
|
|
123
|
+
header={
|
|
124
|
+
<Panel.Header>
|
|
125
|
+
<Text.Headline as="h2">Custom header</Text.Headline>
|
|
126
|
+
<Panel.CloseButton />
|
|
127
|
+
</Panel.Header>
|
|
128
|
+
}
|
|
129
|
+
>
|
|
130
|
+
<Panel.Content>
|
|
131
|
+
<Text.BodyCopy as="p">
|
|
132
|
+
When you supply a custom header via the header prop, you are
|
|
133
|
+
responsible for rendering Panel.CloseButton yourself.
|
|
134
|
+
</Text.BodyCopy>
|
|
135
|
+
</Panel.Content>
|
|
136
|
+
</Panel>
|
|
137
|
+
</LayoutFrame>
|
|
138
|
+
</PanelProvider>
|
|
139
|
+
),
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
export const WithFooter: Story = {
|
|
143
|
+
render: () => (
|
|
144
|
+
<PanelProvider defaultOpen>
|
|
145
|
+
<LayoutFrame>
|
|
146
|
+
<MainContent />
|
|
147
|
+
<Panel
|
|
148
|
+
closeButtonLabel="Close"
|
|
149
|
+
title="Panel title"
|
|
150
|
+
titleId="panel-header"
|
|
151
|
+
aria-labelledby="panel-header"
|
|
152
|
+
>
|
|
153
|
+
<Panel.Content>
|
|
154
|
+
{Array.from({ length: 20 }, (_, i) => (
|
|
155
|
+
<Text.BodyCopy key={i} as="p">
|
|
156
|
+
Scrollable content line {i + 1}
|
|
157
|
+
</Text.BodyCopy>
|
|
158
|
+
))}
|
|
159
|
+
</Panel.Content>
|
|
160
|
+
<Panel.Footer>
|
|
161
|
+
<Box display="flex" justifyContent="flex-end" gap={300}>
|
|
162
|
+
<Button appearance="secondary">Cancel</Button>
|
|
163
|
+
<Button appearance="primary">Save</Button>
|
|
164
|
+
</Box>
|
|
165
|
+
</Panel.Footer>
|
|
166
|
+
</Panel>
|
|
167
|
+
</LayoutFrame>
|
|
168
|
+
</PanelProvider>
|
|
169
|
+
),
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
export const ClosedByDefault: Story = {
|
|
173
|
+
render: () => (
|
|
174
|
+
<PanelProvider>
|
|
175
|
+
<LayoutFrame>
|
|
176
|
+
<MainContent />
|
|
177
|
+
<Panel
|
|
178
|
+
closeButtonLabel="Close"
|
|
179
|
+
title="Panel title"
|
|
180
|
+
titleId="panel-header"
|
|
181
|
+
aria-labelledby="panel-header"
|
|
182
|
+
>
|
|
183
|
+
<PanelBody />
|
|
184
|
+
</Panel>
|
|
185
|
+
</LayoutFrame>
|
|
186
|
+
</PanelProvider>
|
|
187
|
+
),
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
export const MobileBottomSheet: Story = {
|
|
191
|
+
parameters: {
|
|
192
|
+
viewport: { defaultViewport: "mobile1" },
|
|
193
|
+
},
|
|
194
|
+
render: () => (
|
|
195
|
+
<PanelProvider defaultOpen>
|
|
196
|
+
<LayoutFrame>
|
|
197
|
+
<MainContent />
|
|
198
|
+
<Panel
|
|
199
|
+
closeButtonLabel="Close"
|
|
200
|
+
title="Panel title"
|
|
201
|
+
titleId="panel-header"
|
|
202
|
+
aria-labelledby="panel-header"
|
|
203
|
+
>
|
|
204
|
+
<PanelBody />
|
|
205
|
+
</Panel>
|
|
206
|
+
</LayoutFrame>
|
|
207
|
+
</PanelProvider>
|
|
208
|
+
),
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
export const MobileWithFooter: Story = {
|
|
212
|
+
parameters: {
|
|
213
|
+
viewport: { defaultViewport: "mobile1" },
|
|
214
|
+
},
|
|
215
|
+
render: () => (
|
|
216
|
+
<PanelProvider defaultOpen>
|
|
217
|
+
<LayoutFrame>
|
|
218
|
+
<MainContent />
|
|
219
|
+
<Panel
|
|
220
|
+
closeButtonLabel="Close"
|
|
221
|
+
title="Panel title"
|
|
222
|
+
titleId="panel-header"
|
|
223
|
+
aria-labelledby="panel-header"
|
|
224
|
+
>
|
|
225
|
+
<Panel.Content>
|
|
226
|
+
{Array.from({ length: 20 }, (_, i) => (
|
|
227
|
+
<Text.BodyCopy key={i} as="p">
|
|
228
|
+
Scrollable content line {i + 1}
|
|
229
|
+
</Text.BodyCopy>
|
|
230
|
+
))}
|
|
231
|
+
</Panel.Content>
|
|
232
|
+
<Panel.Footer>
|
|
233
|
+
<Box display="flex" justifyContent="flex-end" gap={300}>
|
|
234
|
+
<Button appearance="secondary">Cancel</Button>
|
|
235
|
+
<Button appearance="primary">Save</Button>
|
|
236
|
+
</Box>
|
|
237
|
+
</Panel.Footer>
|
|
238
|
+
</Panel>
|
|
239
|
+
</LayoutFrame>
|
|
240
|
+
</PanelProvider>
|
|
241
|
+
),
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
export const MobileWithActions: Story = {
|
|
245
|
+
parameters: {
|
|
246
|
+
viewport: { defaultViewport: "mobile1" },
|
|
247
|
+
},
|
|
248
|
+
render: () => (
|
|
249
|
+
<PanelProvider defaultOpen>
|
|
250
|
+
<LayoutFrame>
|
|
251
|
+
<MainContent />
|
|
252
|
+
<Panel
|
|
253
|
+
closeButtonLabel="Close"
|
|
254
|
+
title="Panel title"
|
|
255
|
+
titleId="panel-header"
|
|
256
|
+
aria-labelledby="panel-header"
|
|
257
|
+
actions={[
|
|
258
|
+
{
|
|
259
|
+
"aria-label": "Expand",
|
|
260
|
+
iconName: "arrows-pointing-out-outline",
|
|
261
|
+
},
|
|
262
|
+
{ "aria-label": "Share", iconName: "link-outline" },
|
|
263
|
+
]}
|
|
264
|
+
>
|
|
265
|
+
<PanelBody />
|
|
266
|
+
</Panel>
|
|
267
|
+
</LayoutFrame>
|
|
268
|
+
</PanelProvider>
|
|
269
|
+
),
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Exception: use `actionsInHeader` only when the floating rail above the
|
|
274
|
+
* bottom sheet would collide with surrounding UI — for example, nested sheets
|
|
275
|
+
* or snap-point layouts where the fully-expanded sheet leaves no room above.
|
|
276
|
+
*/
|
|
277
|
+
export const MobileWithActionsInHeader: Story = {
|
|
278
|
+
parameters: {
|
|
279
|
+
viewport: { defaultViewport: "mobile1" },
|
|
280
|
+
},
|
|
281
|
+
render: () => (
|
|
282
|
+
<PanelProvider defaultOpen>
|
|
283
|
+
<LayoutFrame>
|
|
284
|
+
<MainContent />
|
|
285
|
+
<Panel
|
|
286
|
+
closeButtonLabel="Close"
|
|
287
|
+
title="Panel title"
|
|
288
|
+
titleId="panel-header"
|
|
289
|
+
aria-labelledby="panel-header"
|
|
290
|
+
actions={[
|
|
291
|
+
{
|
|
292
|
+
"aria-label": "Expand",
|
|
293
|
+
iconName: "arrows-pointing-out-outline",
|
|
294
|
+
},
|
|
295
|
+
{ "aria-label": "Share", iconName: "link-outline" },
|
|
296
|
+
]}
|
|
297
|
+
actionsInHeader
|
|
298
|
+
>
|
|
299
|
+
<PanelBody />
|
|
300
|
+
</Panel>
|
|
301
|
+
</LayoutFrame>
|
|
302
|
+
</PanelProvider>
|
|
303
|
+
),
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
export const CustomHeaderAndFooter: Story = {
|
|
307
|
+
render: () => (
|
|
308
|
+
<PanelProvider defaultOpen>
|
|
309
|
+
<LayoutFrame>
|
|
310
|
+
<MainContent />
|
|
311
|
+
<Panel
|
|
312
|
+
closeButtonLabel="Close"
|
|
313
|
+
header={
|
|
314
|
+
<Panel.Header>
|
|
315
|
+
<Text.Headline as="h2">Custom header</Text.Headline>
|
|
316
|
+
<Panel.CloseButton />
|
|
317
|
+
</Panel.Header>
|
|
318
|
+
}
|
|
319
|
+
>
|
|
320
|
+
<Panel.Content>
|
|
321
|
+
{Array.from({ length: 20 }, (_, i) => (
|
|
322
|
+
<Text.BodyCopy key={i} as="p">
|
|
323
|
+
Scrollable content line {i + 1}
|
|
324
|
+
</Text.BodyCopy>
|
|
325
|
+
))}
|
|
326
|
+
</Panel.Content>
|
|
327
|
+
<Panel.Footer>
|
|
328
|
+
<Box display="flex" justifyContent="flex-end" gap={300}>
|
|
329
|
+
<Button appearance="secondary">Cancel</Button>
|
|
330
|
+
<Button appearance="primary">Save</Button>
|
|
331
|
+
</Box>
|
|
332
|
+
</Panel.Footer>
|
|
333
|
+
</Panel>
|
|
334
|
+
</LayoutFrame>
|
|
335
|
+
</PanelProvider>
|
|
336
|
+
),
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
const ControlledDemo = () => {
|
|
340
|
+
const [open, setOpen] = React.useState(false);
|
|
341
|
+
return (
|
|
342
|
+
<PanelProvider open={open} onOpenChange={setOpen}>
|
|
343
|
+
<LayoutFrame>
|
|
344
|
+
<MainContentWrapper>
|
|
345
|
+
<Box p={500}>
|
|
346
|
+
<Text.Headline as="h1">Controlled panel</Text.Headline>
|
|
347
|
+
<Text.BodyCopy as="p">
|
|
348
|
+
Open state is managed externally via <code>open</code> and{" "}
|
|
349
|
+
<code>onOpenChange</code>.
|
|
350
|
+
</Text.BodyCopy>
|
|
351
|
+
<Box display="flex" gap={300} mt={400}>
|
|
352
|
+
<Button appearance="primary" onClick={() => setOpen(true)}>
|
|
353
|
+
Open
|
|
354
|
+
</Button>
|
|
355
|
+
<Button appearance="secondary" onClick={() => setOpen(false)}>
|
|
356
|
+
Close
|
|
357
|
+
</Button>
|
|
358
|
+
</Box>
|
|
359
|
+
</Box>
|
|
360
|
+
</MainContentWrapper>
|
|
361
|
+
<Panel
|
|
362
|
+
closeButtonLabel="Close"
|
|
363
|
+
title="Controlled panel"
|
|
364
|
+
titleId="controlled-panel-header"
|
|
365
|
+
aria-labelledby="controlled-panel-header"
|
|
366
|
+
>
|
|
367
|
+
<PanelBody />
|
|
368
|
+
</Panel>
|
|
369
|
+
</LayoutFrame>
|
|
370
|
+
</PanelProvider>
|
|
371
|
+
);
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
export const Controlled: Story = {
|
|
375
|
+
render: () => <ControlledDemo />,
|
|
376
|
+
};
|
package/src/Panel.tsx
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import Drawer from "@sproutsocial/seeds-react-drawer";
|
|
3
|
+
import { useIsMobile } from "@sproutsocial/seeds-react-hooks";
|
|
4
|
+
import { usePanelContext, PanelContext } from "./PanelContext";
|
|
5
|
+
import { PanelHeader } from "./PanelHeader";
|
|
6
|
+
import { PanelContent } from "./PanelContent";
|
|
7
|
+
import { PanelCloseButton } from "./PanelCloseButton";
|
|
8
|
+
import { PanelFooter } from "./PanelFooter";
|
|
9
|
+
import { PanelMobileActionsHeader } from "./PanelMobileActionsHeader";
|
|
10
|
+
import { PanelContainer, PanelInner } from "./styles";
|
|
11
|
+
import type { TypePanelProps, TypePanelContext } from "./PanelTypes";
|
|
12
|
+
|
|
13
|
+
const PanelComponent = ({
|
|
14
|
+
children,
|
|
15
|
+
closeButtonLabel,
|
|
16
|
+
header,
|
|
17
|
+
footer,
|
|
18
|
+
title,
|
|
19
|
+
titleId,
|
|
20
|
+
direction = "right",
|
|
21
|
+
width = 384,
|
|
22
|
+
gap = 4,
|
|
23
|
+
mobileBreakpoint,
|
|
24
|
+
zIndex,
|
|
25
|
+
id,
|
|
26
|
+
snapPoints,
|
|
27
|
+
defaultSnapPoint,
|
|
28
|
+
snapPoint,
|
|
29
|
+
onSnapPointChange,
|
|
30
|
+
snapToSequentialPoints,
|
|
31
|
+
actions,
|
|
32
|
+
actionsInHeader,
|
|
33
|
+
"aria-labelledby": ariaLabelledByProp,
|
|
34
|
+
"aria-label": ariaLabel,
|
|
35
|
+
...rest
|
|
36
|
+
}: TypePanelProps) => {
|
|
37
|
+
const panelContext = usePanelContext();
|
|
38
|
+
const { isPanelOpen, closePanel } = panelContext;
|
|
39
|
+
const isMobile = useIsMobile(mobileBreakpoint);
|
|
40
|
+
|
|
41
|
+
// Expose closeButtonLabel through context so PanelCloseButton can read it
|
|
42
|
+
// without prop drilling — same pattern as DrawerContext.
|
|
43
|
+
const enrichedValue = React.useMemo<TypePanelContext>(
|
|
44
|
+
() => ({ ...panelContext, closeButtonLabel }),
|
|
45
|
+
[panelContext, closeButtonLabel]
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
// Freeze the last-rendered children during the collapse so consumers that
|
|
49
|
+
// conditionally render content don't produce an empty panel shell while the
|
|
50
|
+
// flex-basis animation runs.
|
|
51
|
+
const prevChildrenRef = React.useRef<React.ReactNode>(null);
|
|
52
|
+
if (isPanelOpen) {
|
|
53
|
+
prevChildrenRef.current = children;
|
|
54
|
+
}
|
|
55
|
+
const renderedChildren = isPanelOpen ? children : prevChildrenRef.current;
|
|
56
|
+
|
|
57
|
+
// When the consumer relies on the default header, its title element renders
|
|
58
|
+
// with `id={titleId}`. Auto-wire `aria-labelledby` so the dialog/aside has an
|
|
59
|
+
// accessible name without forcing every caller to repeat the id.
|
|
60
|
+
const effectiveAriaLabelledBy =
|
|
61
|
+
ariaLabelledByProp ?? (header == null ? titleId : undefined);
|
|
62
|
+
|
|
63
|
+
if (isMobile) {
|
|
64
|
+
// With `actionsInHeader`, Drawer suppresses the floating rail and expects
|
|
65
|
+
// the consumer to render the actions inside a custom header. Auto-compose
|
|
66
|
+
// one (title + action pills + close pill) when the consumer hasn't
|
|
67
|
+
// supplied their own header; otherwise we'd drop `actions` on the floor.
|
|
68
|
+
const shouldRenderActionsHeader =
|
|
69
|
+
header == null &&
|
|
70
|
+
actionsInHeader === true &&
|
|
71
|
+
!!actions &&
|
|
72
|
+
actions.length > 0;
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<PanelContext.Provider value={enrichedValue}>
|
|
76
|
+
<Drawer
|
|
77
|
+
{...rest}
|
|
78
|
+
open={isPanelOpen}
|
|
79
|
+
onClose={closePanel}
|
|
80
|
+
direction="bottom"
|
|
81
|
+
closeButtonLabel={closeButtonLabel}
|
|
82
|
+
zIndex={zIndex}
|
|
83
|
+
snapPoints={snapPoints}
|
|
84
|
+
defaultSnapPoint={defaultSnapPoint}
|
|
85
|
+
snapPoint={snapPoint}
|
|
86
|
+
onSnapPointChange={onSnapPointChange}
|
|
87
|
+
snapToSequentialPoints={snapToSequentialPoints}
|
|
88
|
+
actions={actions}
|
|
89
|
+
actionsInHeader={actionsInHeader}
|
|
90
|
+
aria-labelledby={effectiveAriaLabelledBy}
|
|
91
|
+
aria-label={ariaLabel}
|
|
92
|
+
>
|
|
93
|
+
{header ??
|
|
94
|
+
(shouldRenderActionsHeader ? (
|
|
95
|
+
<PanelMobileActionsHeader
|
|
96
|
+
title={title}
|
|
97
|
+
titleId={titleId}
|
|
98
|
+
actions={actions!}
|
|
99
|
+
/>
|
|
100
|
+
) : (
|
|
101
|
+
<Drawer.Header title={title} id={titleId} />
|
|
102
|
+
))}
|
|
103
|
+
{children}
|
|
104
|
+
{footer}
|
|
105
|
+
</Drawer>
|
|
106
|
+
</PanelContext.Provider>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<PanelContext.Provider value={enrichedValue}>
|
|
112
|
+
<PanelContainer
|
|
113
|
+
{...rest}
|
|
114
|
+
$isOpen={isPanelOpen}
|
|
115
|
+
$direction={direction}
|
|
116
|
+
$width={width}
|
|
117
|
+
$gap={gap}
|
|
118
|
+
data-qa-panel={id}
|
|
119
|
+
data-qa-panel-isopen={isPanelOpen}
|
|
120
|
+
aria-labelledby={effectiveAriaLabelledBy}
|
|
121
|
+
aria-label={ariaLabel}
|
|
122
|
+
>
|
|
123
|
+
<PanelInner $direction={direction} $width={width}>
|
|
124
|
+
{header ?? <PanelHeader title={title} id={titleId} />}
|
|
125
|
+
{renderedChildren}
|
|
126
|
+
{footer}
|
|
127
|
+
</PanelInner>
|
|
128
|
+
</PanelContainer>
|
|
129
|
+
</PanelContext.Provider>
|
|
130
|
+
);
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
PanelComponent.Header = PanelHeader;
|
|
134
|
+
PanelComponent.Content = PanelContent;
|
|
135
|
+
PanelComponent.CloseButton = PanelCloseButton;
|
|
136
|
+
PanelComponent.Footer = PanelFooter;
|
|
137
|
+
|
|
138
|
+
export const Panel = PanelComponent;
|
|
139
|
+
export default PanelComponent;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import Button from "@sproutsocial/seeds-react-button";
|
|
3
|
+
import Icon from "@sproutsocial/seeds-react-icon";
|
|
4
|
+
import { usePanelContext } from "./PanelContext";
|
|
5
|
+
import type { TypePanelCloseButtonProps } from "./PanelTypes";
|
|
6
|
+
|
|
7
|
+
export const PanelCloseButton = (props: TypePanelCloseButtonProps) => {
|
|
8
|
+
const panelContext = usePanelContext();
|
|
9
|
+
|
|
10
|
+
if (props.render) {
|
|
11
|
+
return props.render(panelContext) ?? null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<Button
|
|
16
|
+
appearance="pill"
|
|
17
|
+
aria-label={panelContext.closeButtonLabel}
|
|
18
|
+
onClick={panelContext.closePanel}
|
|
19
|
+
{...props}
|
|
20
|
+
>
|
|
21
|
+
{props.children || <Icon aria-hidden name="x-outline" />}
|
|
22
|
+
</Button>
|
|
23
|
+
);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
PanelCloseButton.displayName = "Panel.CloseButton";
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { Content } from "./styles";
|
|
3
|
+
import type { TypePanelContentProps } from "./PanelTypes";
|
|
4
|
+
|
|
5
|
+
export const PanelContent = ({ children, ...rest }: TypePanelContentProps) => (
|
|
6
|
+
<Content
|
|
7
|
+
flex="1 1 auto"
|
|
8
|
+
minHeight="0"
|
|
9
|
+
p={450}
|
|
10
|
+
color="text.body"
|
|
11
|
+
data-panel-content=""
|
|
12
|
+
{...rest}
|
|
13
|
+
>
|
|
14
|
+
{children}
|
|
15
|
+
</Content>
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
PanelContent.displayName = "Panel.Content";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import type { TypePanelContext } from "./PanelTypes";
|
|
3
|
+
|
|
4
|
+
export const PanelContext = React.createContext<TypePanelContext | null>(null);
|
|
5
|
+
|
|
6
|
+
export const usePanelContext = (): TypePanelContext => {
|
|
7
|
+
const ctx = React.useContext(PanelContext);
|
|
8
|
+
if (!ctx) {
|
|
9
|
+
throw new Error(
|
|
10
|
+
"usePanelContext must be used within a <PanelProvider>. Wrap the consuming tree in <PanelProvider> to provide isPanelOpen / togglePanel."
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
return ctx;
|
|
14
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { Footer } from "./styles";
|
|
3
|
+
import type { TypePanelFooterProps } from "./PanelTypes";
|
|
4
|
+
|
|
5
|
+
export const PanelFooter = ({ children, ...rest }: TypePanelFooterProps) => (
|
|
6
|
+
<Footer p={450} data-panel-footer="" {...rest}>
|
|
7
|
+
{children}
|
|
8
|
+
</Footer>
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
PanelFooter.displayName = "Panel.Footer";
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import Box from "@sproutsocial/seeds-react-box";
|
|
2
|
+
import Text from "@sproutsocial/seeds-react-text";
|
|
3
|
+
import { usePanelContext } from "./PanelContext";
|
|
4
|
+
import { PanelCloseButton } from "./PanelCloseButton";
|
|
5
|
+
import type { TypePanelHeaderProps } from "./PanelTypes";
|
|
6
|
+
|
|
7
|
+
export const PanelHeader = ({
|
|
8
|
+
title = "",
|
|
9
|
+
id = undefined,
|
|
10
|
+
children,
|
|
11
|
+
render,
|
|
12
|
+
...rest
|
|
13
|
+
}: TypePanelHeaderProps) => {
|
|
14
|
+
const panelContext = usePanelContext();
|
|
15
|
+
|
|
16
|
+
if (render) {
|
|
17
|
+
return render(panelContext);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<Box
|
|
22
|
+
display="flex"
|
|
23
|
+
flex="0 0 auto"
|
|
24
|
+
justifyContent="space-between"
|
|
25
|
+
alignItems="center"
|
|
26
|
+
p={400}
|
|
27
|
+
borderBottom="1px solid"
|
|
28
|
+
borderColor="container.border.base"
|
|
29
|
+
{...rest}
|
|
30
|
+
>
|
|
31
|
+
{children || (
|
|
32
|
+
<>
|
|
33
|
+
<Text.Headline id={id}>{title}</Text.Headline>
|
|
34
|
+
<PanelCloseButton />
|
|
35
|
+
</>
|
|
36
|
+
)}
|
|
37
|
+
</Box>
|
|
38
|
+
);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
PanelHeader.displayName = "Panel.Header";
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import Box from "@sproutsocial/seeds-react-box";
|
|
3
|
+
import Button from "@sproutsocial/seeds-react-button";
|
|
4
|
+
import Drawer from "@sproutsocial/seeds-react-drawer";
|
|
5
|
+
import Icon from "@sproutsocial/seeds-react-icon";
|
|
6
|
+
// eslint-disable-next-line import/no-deprecated
|
|
7
|
+
import Text from "@sproutsocial/seeds-react-text";
|
|
8
|
+
import type { TypeDrawerActionProps } from "@sproutsocial/seeds-react-drawer";
|
|
9
|
+
|
|
10
|
+
interface TypePanelMobileActionsHeaderProps {
|
|
11
|
+
title?: string;
|
|
12
|
+
titleId?: string;
|
|
13
|
+
actions: TypeDrawerActionProps[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Mobile bottom-sheet header used by Panel when `actionsInHeader` is set and
|
|
18
|
+
* no custom `header` is provided. Renders the title on the left and the
|
|
19
|
+
* forwarded actions plus a close button as pills on the right — mirrors the
|
|
20
|
+
* pattern in `seeds-react-peek-in/PeekIn.tsx`. The Drawer's floating rail is
|
|
21
|
+
* suppressed in this combination (`actionsInHeader` opts out), so without this
|
|
22
|
+
* header the actions would render nowhere.
|
|
23
|
+
*/
|
|
24
|
+
export const PanelMobileActionsHeader = ({
|
|
25
|
+
title,
|
|
26
|
+
titleId,
|
|
27
|
+
actions,
|
|
28
|
+
}: TypePanelMobileActionsHeaderProps) => (
|
|
29
|
+
<Drawer.Header
|
|
30
|
+
render={(ctx) => (
|
|
31
|
+
<Box
|
|
32
|
+
display="flex"
|
|
33
|
+
alignItems="center"
|
|
34
|
+
justifyContent="space-between"
|
|
35
|
+
pt={400}
|
|
36
|
+
px={450}
|
|
37
|
+
flex="0 0 auto"
|
|
38
|
+
>
|
|
39
|
+
<Text
|
|
40
|
+
as="h2"
|
|
41
|
+
fontSize={400}
|
|
42
|
+
fontWeight="semibold"
|
|
43
|
+
color="text.headline"
|
|
44
|
+
id={titleId}
|
|
45
|
+
>
|
|
46
|
+
{title}
|
|
47
|
+
</Text>
|
|
48
|
+
<Box display="flex" alignItems="center" gap={200}>
|
|
49
|
+
{actions.map((action, i) => (
|
|
50
|
+
<Button
|
|
51
|
+
key={i}
|
|
52
|
+
appearance="pill"
|
|
53
|
+
aria-label={action["aria-label"]}
|
|
54
|
+
onClick={action.onClick}
|
|
55
|
+
disabled={action.disabled}
|
|
56
|
+
>
|
|
57
|
+
{action.iconName && <Icon aria-hidden name={action.iconName} />}
|
|
58
|
+
</Button>
|
|
59
|
+
))}
|
|
60
|
+
<Button
|
|
61
|
+
appearance="pill"
|
|
62
|
+
aria-label={ctx.closeButtonLabel}
|
|
63
|
+
onClick={ctx.onClose}
|
|
64
|
+
>
|
|
65
|
+
<Icon aria-hidden name="x-outline" />
|
|
66
|
+
</Button>
|
|
67
|
+
</Box>
|
|
68
|
+
</Box>
|
|
69
|
+
)}
|
|
70
|
+
/>
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
PanelMobileActionsHeader.displayName = "PanelMobileActionsHeader";
|