Skip to content
Draft
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
72 changes: 72 additions & 0 deletions pages/flashbar/stack-collapse-control.page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import * as React from 'react';

import Box from '~components/box';
import Flashbar, { FlashbarProps } from '~components/flashbar';
import SpaceBetween from '~components/space-between';

import ScreenshotArea from '../utils/screenshot-area';

const noop = () => void 0;

const i18nStrings: FlashbarProps.I18nStrings = {
ariaLabel: 'Notifications',
notificationBarText: 'Notifications',
notificationBarAriaLabel: 'View all notifications',
errorIconAriaLabel: 'Error',
warningIconAriaLabel: 'Warning',
successIconAriaLabel: 'Success',
infoIconAriaLabel: 'Information',
inProgressIconAriaLabel: 'In progress',
};

const items: FlashbarProps.MessageDefinition[] = [
{
id: 'success',
type: 'success',
statusIconAriaLabel: 'Success',
header: 'Instance created',
dismissible: true,
onDismiss: noop,
dismissLabel: 'Dismiss',
},
{
id: 'warning',
type: 'warning',
statusIconAriaLabel: 'Warning',
header: 'Something weird may have happened...',
},
{
id: 'error',
type: 'error',
statusIconAriaLabel: 'Error',
header: 'Unrecoverable error',
content: 'It all broke, like, really bad.',
},
];

export default function FlashbarStackCollapseControl() {
const [expanded, setExpanded] = React.useState<boolean | null>(null);

return (
<>
<h1>Flashbar stacked group: defaultExpanded + onToggle</h1>
<SpaceBetween size="m">
<Box>
Last toggle event:{' '}
<span id="toggle-state">{expanded === null ? '(none yet)' : expanded ? 'expanded' : 'collapsed'}</span>
</Box>
<ScreenshotArea disableAnimations={true}>
<Flashbar
stackItems={true}
defaultExpanded={true}
onToggle={event => setExpanded(event.detail.expanded)}
i18nStrings={i18nStrings}
items={items}
/>
</ScreenshotArea>
</SpaceBetween>
</>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -14297,7 +14297,27 @@ If you want to clear the selection, use empty array.",
exports[`Components definition for flashbar matches the snapshot: flashbar 1`] = `
{
"dashCaseName": "flashbar",
"events": [],
"events": [
{
"cancelable": false,
"description": "Called when the user expands or collapses the stacked notifications group
by activating the notification bar. Only fires when \`stackItems\` is set to
\`true\`. The event \`detail\` contains the new value of the expanded state.",
"detailInlineType": {
"name": "FlashbarProps.ToggleDetail",
"properties": [
{
"name": "expanded",
"optional": false,
"type": "boolean",
},
],
"type": "object",
},
"detailType": "FlashbarProps.ToggleDetail",
"name": "onToggle",
},
],
"functions": [],
"name": "Flashbar",
"properties": [
Expand All @@ -14308,6 +14328,15 @@ exports[`Components definition for flashbar matches the snapshot: flashbar 1`] =
"optional": true,
"type": "string",
},
{
"description": "Determines whether the stacked notifications group is initially expanded.
Only has an effect when \`stackItems\` is set to \`true\`. Defaults to \`false\`
(the group starts collapsed). This property is applied on the initial
render only; afterwards the expanded state is managed by the component.",
"name": "defaultExpanded",
"optional": true,
"type": "boolean",
},
{
"description": "An object containing all the necessary localized strings required by the component. The object should contain:

Expand Down
42 changes: 42 additions & 0 deletions src/flashbar/__tests__/collapsible.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -644,6 +644,48 @@ describe('Collapsible Flashbar', () => {
});

// Entire interactive element including the counter and the actual <button/> element
describe('Collapsible Flashbar expand/collapse control', () => {
it('starts collapsed by default (defaultExpanded not set)', () => {
const flashbar = renderFlashbar();
expect(flashbar.findItems()).toHaveLength(1);
expect(flashbar.findToggleButton()!.getElement()).toHaveAttribute('aria-expanded', 'false');
});

it('starts expanded when defaultExpanded is true', () => {
const flashbar = renderFlashbar({ defaultExpanded: true });
expect(flashbar.findItems()).toHaveLength(defaultItems.length);
expect(flashbar.findToggleButton()!.getElement()).toHaveAttribute('aria-expanded', 'true');
});

it('starts collapsed when defaultExpanded is false', () => {
const flashbar = renderFlashbar({ defaultExpanded: false });
expect(flashbar.findItems()).toHaveLength(1);
expect(flashbar.findToggleButton()!.getElement()).toHaveAttribute('aria-expanded', 'false');
});

it('fires onToggle with expanded=true when the user expands the stack', () => {
const onToggle = jest.fn();
const flashbar = renderFlashbar({ onToggle });
findNotificationBar(flashbar)!.click();
expect(onToggle).toHaveBeenCalledTimes(1);
expect(onToggle).toHaveBeenCalledWith(expect.objectContaining({ detail: { expanded: true } }));
});

it('fires onToggle with expanded=false when the user collapses the stack', () => {
const onToggle = jest.fn();
const flashbar = renderFlashbar({ defaultExpanded: true, onToggle });
findNotificationBar(flashbar)!.click();
expect(onToggle).toHaveBeenCalledTimes(1);
expect(onToggle).toHaveBeenCalledWith(expect.objectContaining({ detail: { expanded: false } }));
});

it('does not fire onToggle on initial render', () => {
const onToggle = jest.fn();
renderFlashbar({ defaultExpanded: true, onToggle });
expect(onToggle).not.toHaveBeenCalled();
});
});

function findNotificationBar(flashbar: FlashbarWrapper): HTMLElement | undefined {
const element = Array.from(flashbar.getElement().children).find(
element => element instanceof HTMLElement && element.tagName !== 'UL'
Expand Down
15 changes: 12 additions & 3 deletions src/flashbar/collapsible-flashbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import InternalIcon from '../icon/internal';
import { animate, getDOMRects } from '../internal/animate';
import { Transition } from '../internal/components/transition';
import { getVisualContextClassname } from '../internal/components/visual-context';
import { fireNonCancelableEvent } from '../internal/events';
import customCssProps from '../internal/generated/custom-css-properties';
import { useDebounceCallback } from '../internal/hooks/use-debounce-callback';
import { useEffectOnUpdate } from '../internal/hooks/use-effect-on-update';
Expand Down Expand Up @@ -50,11 +51,17 @@ const maxNonCollapsibleItems = 1;

const resizeListenerThrottleDelay = 100;

export default function CollapsibleFlashbar({ items, style, ...restProps }: InternalFlashbarProps) {
export default function CollapsibleFlashbar({
items,
style,
defaultExpanded,
onToggle,
...restProps
}: InternalFlashbarProps) {
const visibleItems = useFlashbarVisibility(items);
const [enteringItems, setEnteringItems] = useState<ReadonlyArray<FlashbarProps.MessageDefinition>>([]);
const [exitingItems, setExitingItems] = useState<ReadonlyArray<FlashbarProps.MessageDefinition>>([]);
const [isFlashbarStackExpanded, setIsFlashbarStackExpanded] = useState(false);
const [isFlashbarStackExpanded, setIsFlashbarStackExpanded] = useState(defaultExpanded ?? false);

const getElementsToAnimate = useCallback(() => {
const flashElements = isFlashbarStackExpanded ? expandedItemRefs.current : collapsedItemRefs.current;
Expand Down Expand Up @@ -104,7 +111,9 @@ export default function CollapsibleFlashbar({ items, style, ...restProps }: Inte
if (!isReducedMotion) {
prepareAnimations();
}
setIsFlashbarStackExpanded(prev => !prev);
const nextExpanded = !isFlashbarStackExpanded;
setIsFlashbarStackExpanded(nextExpanded);
fireNonCancelableEvent(onToggle, { expanded: nextExpanded });
}

const debouncedFocus = useDebounceCallback(focusFlashById, FOCUS_DEBOUNCE_DELAY);
Expand Down
20 changes: 20 additions & 0 deletions src/flashbar/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import React from 'react';
import { ButtonProps } from '../button/interfaces';
import { ErrorContext } from '../types/analytics';
import { BaseComponentProps } from '../types/base-component';
import { NonCancelableEventHandler } from '../types/events';

export namespace FlashbarProps {
export interface MessageDefinition {
Expand Down Expand Up @@ -34,6 +35,10 @@ export namespace FlashbarProps {
crossServicePersistence?: boolean;
}

export interface ToggleDetail {
expanded: boolean;
}

export interface I18nStrings {
ariaLabel?: string;
errorIconAriaLabel?: string;
Expand Down Expand Up @@ -197,6 +202,21 @@ export interface FlashbarProps extends BaseComponentProps {
*/
stackItems?: boolean;

/**
* Determines whether the stacked notifications group is initially expanded.
* Only has an effect when `stackItems` is set to `true`. Defaults to `false`
* (the group starts collapsed). This property is applied on the initial
* render only; afterwards the expanded state is managed by the component.
*/
defaultExpanded?: boolean;

/**
* Called when the user expands or collapses the stacked notifications group
* by activating the notification bar. Only fires when `stackItems` is set to
* `true`. The event `detail` contains the new value of the expanded state.
*/
onToggle?: NonCancelableEventHandler<FlashbarProps.ToggleDetail>;

/**
* An object containing all the necessary localized strings required by the component. The object should contain:
*
Expand Down
Loading