Skip to content

feat: Drawer header actions #3462

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
merged 14 commits into from
May 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@
{
"path": "lib/components/internal/widget-exports.js",
"brotli": false,
"limit": "775 kB",
"limit": "776 kB",
"ignore": "react-dom"
}
],
Expand Down
22 changes: 15 additions & 7 deletions pages/app-layout/utils/external-widget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import React, { useEffect, useImperativeHandle, useRef, useState } from 'react';
import ReactDOM, { unmountComponentAtNode } from 'react-dom';

import ButtonDropdown from '~components/button-dropdown';
import Drawer from '~components/drawer';
import awsuiPlugins from '~components/internal/plugins';

Expand Down Expand Up @@ -153,13 +154,20 @@ awsuiPlugins.appLayout.registerDrawer({

mountContent: (container, mountContext) => {
ReactDOM.render(
<AutoIncrementCounter onVisibilityChange={mountContext?.onVisibilityChange}>
global widget content circle 1
{new Array(100).fill(null).map((_, index) => (
<div key={index}>{index}</div>
))}
<div data-testid="circle-global-bottom-content">circle-global bottom content</div>
</AutoIncrementCounter>,
<Drawer
header={<h2>Global drawer</h2>}
headerActions={
<ButtonDropdown items={[{ id: 'settings', text: 'Settings' }]} ariaLabel="Control drawer" variant="icon" />
}
>
<AutoIncrementCounter onVisibilityChange={mountContext?.onVisibilityChange}>
global widget content circle 1
{new Array(100).fill(null).map((_, index) => (
<div key={index}>{index}</div>
))}
<div data-testid="circle-global-bottom-content">circle-global bottom content</div>
</AutoIncrementCounter>
</Drawer>,
container
);
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8451,6 +8451,11 @@ It should contain the only \`h2\` used in the drawer.",
"isDefault": false,
"name": "header",
},
{
"description": "Actions for the header. Available only if you specify the \`header\` property.",
"isDefault": false,
"name": "headerActions",
},
],
"releaseStatus": "stable",
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,7 @@ exports[`test-utils selectors 1`] = `
],
"drawer": [
"awsui_drawer_1sxt8",
"awsui_header-actions_1sxt8",
"awsui_header_1sxt8",
"awsui_test-utils-drawer-content_1sxt8",
],
Expand Down
16 changes: 16 additions & 0 deletions src/drawer/__tests__/drawer.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,22 @@ test('renders header if it is provided', () => {
expect(wrapper.findContent()!.getElement()).toHaveTextContent('there is a header above');
});

test('renders header actions if it is provided', () => {
const wrapper = renderDrawer(
<Drawer header="Bla bla" headerActions={<div>Header actions</div>}>
there is a header above
</Drawer>
);
expect(wrapper.findHeader()!.getElement()).toHaveTextContent('Bla bla');
expect(wrapper.findHeaderActions()!.getElement()).toHaveTextContent('Header actions');
expect(wrapper.findContent()!.getElement()).toHaveTextContent('there is a header above');
});

test('does not render header actions if header is not provided', () => {
const wrapper = renderDrawer(<Drawer headerActions={<div>Header actions</div>}>there is a header above</Drawer>);
expect(wrapper.findHeaderActions()).toBeFalsy();
});

test('renders loading state', () => {
const { container } = render(<Drawer loading={true} i18nStrings={{ loadingText: 'Loading content' }} />);
expect(createWrapper(container).findStatusIndicator()!.getElement()).toHaveTextContent('Loading content');
Expand Down
9 changes: 8 additions & 1 deletion src/drawer/implementation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export function DrawerImplementation({
i18nStrings,
disableContentPaddings,
__internalRootRef,
headerActions,
...restProps
}: DrawerInternalProps) {
const baseProps = getBaseProps(restProps);
Expand All @@ -32,6 +33,7 @@ export function DrawerImplementation({
...baseProps,
className: clsx(baseProps.className, styles.drawer, isToolbar && styles['with-toolbar']),
};

return loading ? (
<div
{...containerProps}
Expand All @@ -46,7 +48,12 @@ export function DrawerImplementation({
</div>
) : (
<div {...containerProps} ref={__internalRootRef}>
{header && <div className={styles.header}>{header}</div>}
{header && (
<div className={styles.header}>
{header}
{headerActions && <div className={styles['header-actions']}>{headerActions}</div>}
</div>
)}
<div
className={clsx(
styles['test-utils-drawer-content'],
Expand Down
5 changes: 5 additions & 0 deletions src/drawer/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ export interface DrawerProps extends BaseComponentProps {
* @i18n
*/
i18nStrings?: I18nStrings;

/**
* Actions for the header. Available only if you specify the `header` property.
*/
headerActions?: React.ReactNode;
}

interface I18nStrings {
Expand Down
14 changes: 14 additions & 0 deletions src/drawer/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,14 @@

.header {
@include styles.font-panel-header;
display: flex;
justify-content: space-between;
color: awsui.$color-text-heading-default;
padding-block: awsui.$space-panel-header-vertical;
padding-inline: awsui.$space-panel-side-left calc(#{awsui.$space-xl} + #{awsui.$space-scaled-xxl});
// padding to make sure the header doesn't overlap with the close icon
border-block-end: awsui.$border-divider-section-width solid awsui.$color-border-panel-header;

.with-toolbar > & {
border-color: transparent;
margin-block-end: 0px;
Expand All @@ -36,6 +39,17 @@
/* stylelint-enable @cloudscape-design/no-implicit-descendant, selector-max-type */
}

.header-actions {
display: inline-flex;
align-items: start;
z-index: 1;
/*
Compensate for the difference between the runtime drawer's and the drawer component's heading
to ensure the header actions are vertically aligned
*/
margin-block-start: -6px;
}

.content-with-paddings:not(:empty) {
padding-block-start: awsui.$space-panel-content-top;
padding-inline-start: awsui.$space-panel-side-left;
Expand Down
4 changes: 4 additions & 0 deletions src/test-utils/dom/drawer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ export default class DrawerWrapper extends ComponentWrapper {
return this.findByClassName(styles.header);
}

findHeaderActions(): ElementWrapper | null {
return this.findByClassName(styles['header-actions']);
}

findContent(): ElementWrapper | null {
return this.findByClassName(styles['test-utils-drawer-content']);
}
Expand Down
Loading