Skip to content

Commit

Permalink
chore: Add runtime actions discovery to alert and flashbar
Browse files Browse the repository at this point in the history
  • Loading branch information
just-boris committed Aug 9, 2023
1 parent fc92170 commit 92eeabb
Show file tree
Hide file tree
Showing 18 changed files with 787 additions and 66 deletions.
74 changes: 74 additions & 0 deletions pages/alert/runtime-action.page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
import Alert, { AlertProps } from '~components/alert';
import Button from '~components/button';
import SpaceBetween from '~components/space-between';
import awsuiPlugins from '~components/internal/plugins';
import ScreenshotArea from '../utils/screenshot-area';
import createPermutations from '../utils/permutations';
import PermutationsView from '../utils/permutations-view';

awsuiPlugins.alert.registerAction({
id: 'awsui/alert-test-action',
mountContent: (container, context) => {
if (context.type !== 'error') {
return;
}
render(
<Button
iconName="status-info"
onClick={() => {
alert(
[
'Content',
`Type: ${context.type}`,
`Header: ${context.headerRef.current?.textContent}`,
`Content: ${context.contentRef.current?.textContent}`,
].join('\n')
);
}}
>
Runtime button
</Button>,
container
);
},
unmountContent: container => unmountComponentAtNode(container),
});

/* eslint-disable react/jsx-key */
const permutations = createPermutations<AlertProps>([
{
dismissible: [true, false],
header: ['Alert'],
children: ['Content'],
type: ['success', 'error'],
action: [
null,
<Button>Action</Button>,
<SpaceBetween direction="horizontal" size="xs">
<Button>Action 1</Button>
<Button>Action 2</Button>
</SpaceBetween>,
],
},
]);
/* eslint-enable react/jsx-key */

export default function () {
return (
<>
<h1>Alert runtime actions</h1>
<ScreenshotArea>
<PermutationsView
permutations={permutations}
render={permutation => (
<Alert statusIconAriaLabel={permutation.type} dismissAriaLabel="Dismiss" {...permutation} />
)}
/>
</ScreenshotArea>
</>
);
}
74 changes: 74 additions & 0 deletions pages/flashbar/runtime-action.page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
import Flashbar, { FlashbarProps } from '~components/flashbar';
import Button from '~components/button';
import SpaceBetween from '~components/space-between';
import awsuiPlugins from '~components/internal/plugins';
import ScreenshotArea from '../utils/screenshot-area';
import createPermutations from '../utils/permutations';
import PermutationsView from '../utils/permutations-view';

awsuiPlugins.flashbar.registerAction({
id: 'awsui/flashbar-test-action',
mountContent: (container, context) => {
if (context.type !== 'error') {
return;
}
render(
<Button
iconName="status-info"
onClick={() => {
alert(
[
'Content',
`Type: ${context.type}`,
`Header: ${context.headerRef.current?.textContent}`,
`Content: ${context.contentRef.current?.textContent}`,
].join('\n')
);
}}
>
Runtime button
</Button>,
container
);
},
unmountContent: container => unmountComponentAtNode(container),
});

/* eslint-disable react/jsx-key */
const permutations = createPermutations<FlashbarProps.MessageDefinition>([
{
dismissible: [true, false],
header: ['Flash message'],
content: ['Content'],
type: ['success', 'error'],
action: [
null,
<Button>Action</Button>,
<SpaceBetween direction="horizontal" size="xs">
<Button>Action 1</Button>
<Button>Action 2</Button>
</SpaceBetween>,
],
},
]);
/* eslint-enable react/jsx-key */

export default function () {
return (
<>
<h1>Alert runtime actions</h1>
<ScreenshotArea>
<PermutationsView
permutations={permutations}
render={permutation => (
<Flashbar items={[{ ...permutation, statusIconAriaLabel: permutation.type, dismissLabel: 'Dismiss' }]} />
)}
/>
</ScreenshotArea>
</>
);
}
119 changes: 119 additions & 0 deletions src/alert/__tests__/runtime-action.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import * as React from 'react';
import { act, fireEvent, render, screen } from '@testing-library/react';
import Alert from '../../../lib/components/alert';
import awsuiPlugins from '../../../lib/components/internal/plugins';
import { ActionConfig } from '../../../lib/components/internal/plugins/action-buttons-controller';
import { awsuiPluginsInternal } from '../../../lib/components/internal/plugins/api';

const defaultAction: ActionConfig = {
id: 'test-action',
mountContent: container => {
const button = document.createElement('button');
button.dataset.testid = 'test-action';
container.appendChild(button);
},
unmountContent: container => (container.innerHTML = ''),
};

function delay() {
return act(() => new Promise(resolve => setTimeout(resolve)));
}

afterEach(() => {
awsuiPluginsInternal.alert.clearAction();
});

test('renders runtime action button initially', async () => {
awsuiPlugins.alert.registerAction(defaultAction);
render(<Alert />);
await delay();
expect(screen.queryByTestId('test-action')).toBeTruthy();
});

test('renders runtime action button asynchronously', async () => {
render(<Alert />);
await delay();
expect(screen.queryByTestId('test-action')).toBeFalsy();
awsuiPlugins.alert.registerAction(defaultAction);
await delay();
expect(screen.queryByTestId('test-action')).toBeTruthy();
});

test('renders runtime action along with the props one', async () => {
awsuiPlugins.alert.registerAction(defaultAction);
render(<Alert action={<button data-testid="own-button">test</button>} />);
await delay();
expect(screen.queryByTestId('own-button')).toBeTruthy();
expect(screen.queryByTestId('test-action')).toBeTruthy();
});

test('renders runtime action button on multiple instances', async () => {
awsuiPlugins.alert.registerAction(defaultAction);
render(
<>
<Alert />
<Alert />
</>
);
await delay();
expect(screen.queryAllByTestId('test-action')).toHaveLength(2);
});

test('propagates alert context into callback', async () => {
const onClick = jest.fn();
const testAction: ActionConfig = {
...defaultAction,
mountContent: (container, context) => {
const button = document.createElement('button');
button.dataset.testid = 'test-action';
button.onclick = () => onClick(context);
container.appendChild(button);
},
};
awsuiPlugins.alert.registerAction(testAction);
render(<Alert header="Test header">Test content</Alert>);
await delay();
fireEvent.click(screen.getByTestId('test-action'));
expect(onClick).toHaveBeenCalledWith({
type: 'info',
headerRef: { current: expect.any(HTMLElement) },
contentRef: { current: expect.any(HTMLElement) },
});
});

test('allows skipping rendering actions', async () => {
const testAction: ActionConfig = {
...defaultAction,
mountContent: (container, context) => {
if (context.type !== 'error') {
return;
}
defaultAction.mountContent(container, context);
},
};
awsuiPlugins.alert.registerAction(testAction);
const { rerender } = render(<Alert type="info" />);
await delay();
expect(screen.queryByTestId('test-action')).toBeFalsy();
rerender(<Alert type="error" />);
await delay();
expect(screen.queryByTestId('test-action')).toBeTruthy();
});

test('cleans up on unmount', async () => {
const testAction: ActionConfig = {
...defaultAction,
mountContent: jest.fn(),
unmountContent: jest.fn(),
};
awsuiPlugins.alert.registerAction(testAction);
const { rerender } = render(<Alert />);
await delay();
expect(testAction.mountContent).toHaveBeenCalledTimes(1);
expect(testAction.unmountContent).toHaveBeenCalledTimes(0);
rerender(<></>);
expect(testAction.mountContent).toHaveBeenCalledTimes(1);
expect(testAction.unmountContent).toHaveBeenCalledTimes(1);
});
49 changes: 49 additions & 0 deletions src/alert/actions-wrapper/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React from 'react';
import styles from './styles.css.js';
import InternalButton, { InternalButtonProps } from '../../button/internal';

function createActionButton(
testUtilClasses: ActionsWrapperProps['testUtilClasses'],
action: React.ReactNode,
buttonText: React.ReactNode,
onButtonClick: InternalButtonProps['onClick']
) {
if (!action && buttonText) {
action = (
<InternalButton className={testUtilClasses.actionButton} onClick={onButtonClick} formAction="none">
{buttonText}
</InternalButton>
);
}
return action ? <div className={testUtilClasses.action}>{action}</div> : null;
}

interface ActionsWrapperProps {
action: React.ReactNode;
discoveredAction: React.ReactNode;
buttonText: React.ReactNode;
onButtonClick: InternalButtonProps['onClick'];
testUtilClasses: { action: string; actionButton: string };
}

export function ActionsWrapper({
testUtilClasses,
action,
discoveredAction,
buttonText,
onButtonClick,
}: ActionsWrapperProps) {
const actionButton = createActionButton(testUtilClasses, action, buttonText, onButtonClick);
if (!actionButton && !discoveredAction) {
return null;
}

return (
<div className={styles.root}>
{actionButton}
{discoveredAction}
</div>
);
}
17 changes: 17 additions & 0 deletions src/alert/actions-wrapper/styles.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/

@use '../../internal/styles/tokens' as awsui;

.root {
margin-left: awsui.$space-alert-action-left;
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: awsui.$space-xs;
& > :empty {
display: none;
}
}
Loading

0 comments on commit 92eeabb

Please sign in to comment.