Skip to content

Commit fd7b0ab

Browse files
jperalstimogasdagethinwebsterjust-borisavinashbot
authored
feat: Support custom elements in Split Panel header (#3689)
Co-authored-by: Timo <[email protected]> Co-authored-by: Gethin Webster <[email protected]> Co-authored-by: Boris Serdiuk <[email protected]> Co-authored-by: Avinash Dwarapu <[email protected]> Co-authored-by: Nemes-Teodora <[email protected]> Co-authored-by: Andrei Zhaleznichenka <[email protected]>
1 parent ace37cf commit fd7b0ab

File tree

18 files changed

+909
-113
lines changed

18 files changed

+909
-113
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@
167167
{
168168
"path": "lib/components/internal/widget-exports.js",
169169
"brotli": false,
170-
"limit": "830 kB",
170+
"limit": "835 kB",
171171
"ignore": "react-dom"
172172
}
173173
],
Lines changed: 324 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,324 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
import React, { useContext, useEffect, useRef, useState } from 'react';
4+
5+
import AppLayout, { AppLayoutProps } from '~components/app-layout';
6+
import Badge from '~components/badge';
7+
import Box from '~components/box';
8+
import Button from '~components/button';
9+
import ButtonDropdown from '~components/button-dropdown';
10+
import ColumnLayout from '~components/column-layout';
11+
import FormField from '~components/form-field/internal';
12+
import Header from '~components/header';
13+
import Input from '~components/input';
14+
import FocusLock, { FocusLockRef } from '~components/internal/components/focus-lock';
15+
import Link from '~components/link';
16+
import SpaceBetween from '~components/space-between';
17+
import SplitPanel from '~components/split-panel';
18+
19+
import AppContext, { AppContextType } from '../app/app-context';
20+
import ScreenshotArea from '../utils/screenshot-area';
21+
import { Breadcrumbs, ScrollableDrawerContent, Tools } from './utils/content-blocks';
22+
import labels from './utils/labels';
23+
import { splitPaneli18nStrings } from './utils/strings';
24+
import * as toolsContent from './utils/tools-content';
25+
26+
import styles from './styles.scss';
27+
28+
type SplitPanelDemoContext = React.Context<
29+
AppContextType<{
30+
ariaLabel?: string;
31+
description?: string;
32+
editableHeader: boolean;
33+
headerText?: string;
34+
linkedHeader?: boolean;
35+
renderActionsButtonDropdown: boolean;
36+
renderActionsButtonLink: boolean;
37+
renderBeforeButtons: boolean;
38+
renderBeforeBadge: boolean;
39+
renderInfoLink: boolean;
40+
splitPanelOpen: boolean;
41+
splitPanelPosition: AppLayoutProps.SplitPanelPreferences['position'];
42+
}>
43+
>;
44+
45+
function EditableHeader({ onChange, value }: { onChange: (text: string) => void; value: string }) {
46+
const [internalValue, setInternalValue] = useState(value);
47+
const [editing, setEditing] = useState(false);
48+
const inputRef = useRef<HTMLInputElement>(null);
49+
const focusLockRef = useRef<FocusLockRef>(null);
50+
51+
useEffect(() => {
52+
if (editing) {
53+
inputRef.current?.focus();
54+
}
55+
}, [editing]);
56+
57+
return editing ? (
58+
<span role="dialog" aria-label="Edit resource name" style={{ display: 'inline-block' }}>
59+
<FocusLock ref={focusLockRef}>
60+
<form
61+
onSubmit={() => {
62+
onChange(internalValue);
63+
setEditing(false);
64+
}}
65+
>
66+
<SpaceBetween direction="horizontal" size="xxs">
67+
<SpaceBetween direction="horizontal" size="s" alignItems="center">
68+
<Box>
69+
<label id="edit-resource-name-label">Resource name</label>
70+
</Box>
71+
<Input
72+
ariaLabelledby="edit-resource-name-label"
73+
value={internalValue}
74+
onChange={({ detail }) => setInternalValue(detail.value)}
75+
ref={inputRef}
76+
/>
77+
</SpaceBetween>
78+
<Button variant="icon" iconName="check" formAction="submit" ariaLabel="Submit" />
79+
<Button
80+
variant="icon"
81+
iconName="close"
82+
formAction="none"
83+
ariaLabel="Submit"
84+
onClick={() => setEditing(false)}
85+
/>
86+
</SpaceBetween>
87+
</form>
88+
</FocusLock>
89+
</span>
90+
) : (
91+
<span className={styles['split-panel-header-margin']}>
92+
<span>{value}</span>{' '}
93+
<Button
94+
variant="inline-icon"
95+
iconName="edit"
96+
ariaLabel="Edit resource name"
97+
onClick={() => setEditing(true)}
98+
></Button>
99+
</span>
100+
);
101+
}
102+
103+
export default function () {
104+
const { urlParams, setUrlParams } = useContext(AppContext as SplitPanelDemoContext);
105+
const [toolsOpen, setToolsOpen] = useState(false);
106+
107+
const {
108+
ariaLabel,
109+
description,
110+
editableHeader,
111+
linkedHeader,
112+
renderActionsButtonDropdown,
113+
renderActionsButtonLink,
114+
renderBeforeBadge,
115+
renderBeforeButtons,
116+
renderInfoLink,
117+
splitPanelOpen,
118+
splitPanelPosition,
119+
} = urlParams;
120+
121+
// Initalize with a known header text for a11y compliance if not provided.
122+
const headerText = urlParams.headerText === undefined ? 'Header text' : urlParams.headerText;
123+
124+
const renderHeaderTextAsLink = !editableHeader && linkedHeader && headerText;
125+
const renderActions = renderActionsButtonDropdown || renderActionsButtonLink;
126+
const renderBefore = editableHeader || linkedHeader || renderBeforeBadge || renderBeforeButtons;
127+
const renderHeaderTextInBeforeSlot = editableHeader || linkedHeader || renderBeforeButtons;
128+
129+
return (
130+
<ScreenshotArea gutters={false} disableAnimations={true}>
131+
<AppLayout
132+
ariaLabels={labels}
133+
breadcrumbs={<Breadcrumbs />}
134+
navigationHide={true}
135+
tools={<Tools>{toolsContent.long}</Tools>}
136+
toolsOpen={toolsOpen}
137+
splitPanelOpen={splitPanelOpen}
138+
onSplitPanelToggle={({ detail }) => setUrlParams({ ...urlParams, splitPanelOpen: detail.open })}
139+
splitPanelPreferences={{
140+
position: splitPanelPosition,
141+
}}
142+
onSplitPanelPreferencesChange={event => {
143+
const { position } = event.detail;
144+
setUrlParams({ splitPanelPosition: position === 'side' ? position : undefined });
145+
}}
146+
onToolsChange={({ detail }) => setToolsOpen(detail.open)}
147+
splitPanel={
148+
<SplitPanel
149+
header={renderHeaderTextInBeforeSlot ? '' : headerText}
150+
i18nStrings={splitPaneli18nStrings}
151+
headerActions={
152+
renderActions && (
153+
<SpaceBetween direction="horizontal" size="xs" alignItems="center">
154+
{renderActionsButtonLink && <Link>Action</Link>}
155+
{renderActionsButtonDropdown && (
156+
<ButtonDropdown
157+
items={[{ id: 'settings', text: 'Settings' }]}
158+
ariaLabel="Control drawer"
159+
variant="icon"
160+
expandToViewport={true}
161+
/>
162+
)}
163+
</SpaceBetween>
164+
)
165+
}
166+
headerBefore={
167+
renderBefore && (
168+
<span
169+
className={
170+
renderBeforeButtons
171+
? styles['split-panel-header-full-width']
172+
: renderHeaderTextAsLink
173+
? styles['split-panel-header-margin']
174+
: undefined
175+
}
176+
>
177+
<span>
178+
{renderBeforeBadge && (
179+
<Box display="inline-block" margin={{ right: renderHeaderTextInBeforeSlot ? 'xs' : 'n' }}>
180+
<Badge>3</Badge>
181+
</Box>
182+
)}
183+
{editableHeader && (
184+
<EditableHeader
185+
value={headerText || ''}
186+
onChange={value => setUrlParams({ ...urlParams, headerText: value })}
187+
/>
188+
)}
189+
{renderHeaderTextAsLink && (
190+
<span className={renderBeforeButtons ? styles['split-panel-header-margin'] : undefined}>
191+
<Link fontSize="inherit" href="#">
192+
{headerText}
193+
</Link>
194+
</span>
195+
)}
196+
{renderHeaderTextInBeforeSlot && !editableHeader && !renderHeaderTextAsLink && (
197+
<span className={styles['split-panel-header-margin']}>{headerText}</span>
198+
)}
199+
</span>
200+
{renderBeforeButtons && (
201+
<SpaceBetween direction="horizontal" size="xs">
202+
<Button>Button</Button> <Button>Button</Button>
203+
</SpaceBetween>
204+
)}
205+
</span>
206+
)
207+
}
208+
headerDescription={description}
209+
headerInfo={
210+
renderInfoLink && (
211+
<Link variant="info" onFollow={() => setToolsOpen(true)}>
212+
Info
213+
</Link>
214+
)
215+
}
216+
ariaLabel={ariaLabel}
217+
>
218+
<ScrollableDrawerContent />
219+
</SplitPanel>
220+
}
221+
content={
222+
<>
223+
<div style={{ marginBottom: '1rem' }}>
224+
<Header variant="h1">Split panel with custom header elements</Header>
225+
</div>
226+
<SpaceBetween size="l">
227+
<ColumnLayout columns={2}>
228+
<FormField label="beforeHeader slot">
229+
<SpaceBetween size="xxs">
230+
<label>
231+
<input
232+
type="checkbox"
233+
checked={renderBeforeBadge}
234+
onChange={({ target }) => setUrlParams({ ...urlParams, renderBeforeBadge: target.checked })}
235+
/>{' '}
236+
Badge
237+
</label>
238+
<label>
239+
<input
240+
type="checkbox"
241+
checked={renderBeforeButtons}
242+
onChange={({ target }) => setUrlParams({ ...urlParams, renderBeforeButtons: target.checked })}
243+
/>{' '}
244+
Buttons
245+
</label>
246+
<label>
247+
<input
248+
type="checkbox"
249+
checked={editableHeader}
250+
onChange={({ target }) => setUrlParams({ ...urlParams, editableHeader: target.checked })}
251+
/>{' '}
252+
Editable header text
253+
</label>
254+
<label>
255+
<input
256+
type="checkbox"
257+
checked={linkedHeader}
258+
disabled={editableHeader}
259+
onChange={({ target }) => setUrlParams({ ...urlParams, linkedHeader: target.checked })}
260+
/>{' '}
261+
Header text as link
262+
</label>
263+
</SpaceBetween>
264+
</FormField>
265+
<FormField label="headerActions slot">
266+
<SpaceBetween size="xxs">
267+
<label>
268+
<input
269+
type="checkbox"
270+
checked={renderActionsButtonDropdown}
271+
onChange={({ target }) =>
272+
setUrlParams({ ...urlParams, renderActionsButtonDropdown: target.checked })
273+
}
274+
/>{' '}
275+
Button dropdown
276+
</label>
277+
<label>
278+
<input
279+
type="checkbox"
280+
checked={renderActionsButtonLink}
281+
onChange={({ target }) =>
282+
setUrlParams({ ...urlParams, renderActionsButtonLink: target.checked })
283+
}
284+
/>{' '}
285+
Inline link button
286+
</label>
287+
</SpaceBetween>
288+
</FormField>
289+
</ColumnLayout>
290+
<FormField label="Header text">
291+
<Input
292+
value={headerText || ''}
293+
onChange={({ detail }) => setUrlParams({ ...urlParams, headerText: detail.value })}
294+
/>
295+
</FormField>
296+
<FormField label="Description">
297+
<Input
298+
value={description || ''}
299+
onChange={({ detail }) => setUrlParams({ ...urlParams, description: detail.value })}
300+
/>
301+
</FormField>
302+
<FormField label="ARIA label">
303+
<Input
304+
value={ariaLabel || ''}
305+
onChange={({ detail }) => setUrlParams({ ...urlParams, ariaLabel: detail.value })}
306+
/>
307+
</FormField>
308+
<FormField>
309+
<label>
310+
<input
311+
type="checkbox"
312+
checked={renderInfoLink}
313+
onChange={({ target }) => setUrlParams({ ...urlParams, renderInfoLink: target.checked })}
314+
/>{' '}
315+
Info link
316+
</label>
317+
</FormField>
318+
</SpaceBetween>
319+
</>
320+
}
321+
/>
322+
</ScreenshotArea>
323+
);
324+
}

pages/app-layout/styles.scss

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,3 +105,17 @@
105105
border-block-start: 2px solid grey;
106106
padding-block: awsui.$space-scaled-s;
107107
}
108+
109+
.split-panel-header-margin {
110+
display: inline-block;
111+
margin-block-start: calc(#{awsui.$space-scaled-xxs} + 1px);
112+
}
113+
114+
.split-panel-header-full-width {
115+
display: flex;
116+
flex-wrap: wrap;
117+
justify-content: space-between;
118+
column-gap: awsui.$space-scaled-xs;
119+
row-gap: awsui.$space-scaled-xxs;
120+
align-items: flex-start;
121+
}

0 commit comments

Comments
 (0)