Skip to content
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 @@ -183,7 +183,7 @@
{
"path": "lib/components/internal/widget-exports.js",
"brotli": false,
"limit": "1460 kB",
"limit": "1470 kB",
"ignore": "react-dom"
}
],
Expand Down
60 changes: 60 additions & 0 deletions pages/flashbar/style-api-v2.page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React from 'react';

import { Flashbar, FlashbarProps } from '~components';

import { SimplePage } from '../app/templates';

import styles from './style-api-v2.scss';

const noop = () => void 0;

// One class per message type through the item callback; the collapsed stack applies it to the placeholders too.
const itemClassName = ({ item }: { item: FlashbarProps.MessageDefinition }) =>
item.loading ? styles['item-loading'] : styles[`item-${item.type ?? 'info'}`];

const dismissible = { dismissible: true, dismissLabel: 'Dismiss', onDismiss: noop };

const itemsByType: FlashbarProps.MessageDefinition[] = [
{ type: 'success', content: 'Instance started.', id: 'success', ...dismissible },
{ type: 'error', content: 'Deployment failed.', id: 'error', ...dismissible },
{ type: 'warning', content: 'Certificate expires soon.', id: 'warning', ...dismissible },
{ type: 'info', content: 'New features available.', id: 'info', ...dismissible },
{ loading: true, content: 'Creating snapshot.', id: 'loading' },
];

export default function () {
return (
<SimplePage title="Flashbar - Style API v2" screenshotArea={{ disableAnimations: true }} i18n={{}}>
<Flashbar
items={[
{
type: 'success',
header: 'Deployment complete',
content: 'All 12 instances are running.',
id: 'item',
...dismissible,
},
]}
{...{ styleClassNames: { item: styles.item, icon: styles.icon, dismissButton: styles['dismiss-button'] } }}
/>
<Flashbar
items={itemsByType}
{...{ styleClassNames: { item: itemClassName, icon: styles.icon, dismissButton: styles['dismiss-button'] } }}
/>
<Flashbar
stackItems={true}
items={itemsByType.slice(0, 3)}
{...{
styleClassNames: {
item: itemClassName,
icon: styles.icon,
dismissButton: styles['dismiss-button'],
notificationBar: styles['notification-bar'],
},
}}
/>
</SimplePage>
);
}
97 changes: 97 additions & 0 deletions pages/flashbar/style-api-v2.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/*
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/

@use '~components/styling/flashbar';
@use '~components/styling/button';
@use '../style-api/palette' as palette;

// Every item row and the focus-ring tokens in one class.
@mixin item($accent, $background, $color: palette.$text) {
@include flashbar.item(
$color: $color,
$background: $background,
$border-color: $accent,
$border-width: 2px,
$border-style: solid,
$border-radius: 8px,
$box-shadow: 0 4px 12px palette.$subtle-active,
$padding-block: 12px,
$padding-inline: 20px,
$focus-ring-color: $accent,
$focus-ring-width: 3px,
$focus-ring-radius: 10px
);
}

.item {
@include item(palette.$genai, palette.$genai-surface, palette.$genai-text);
}

.icon {
@include flashbar.icon($color: palette.$genai);
}

.dismiss-button {
@include button.root($color: palette.$genai-text, $focus-ring-color: palette.$genai);

&:hover,
&:focus {
@include button.root($color: palette.$genai);
}
}

// One class per message type, resolved through the item callback.
.item-success {
@include item(palette.$success, palette.$success-surface);
}

.item-error {
@include item(palette.$error, palette.$error-surface);
}

.item-warning {
@include item(palette.$warning, palette.$warning-surface);
}

.item-info {
@include item(palette.$info, palette.$info-surface);
}

.item-loading {
@include item(palette.$text-muted, palette.$surface-alt);
}

.notification-bar {
@include flashbar.notificationBar(
$color: palette.$genai-text,
$background: palette.$genai-surface,
$border-color: palette.$genai,
$border-width: 2px,
$border-style: solid,
$border-radius: 8px,
$box-shadow: 0 4px 12px palette.$subtle-active,
$padding-block: 6px,
$padding-inline: 24px,
$focus-ring-color: palette.$genai,
$focus-ring-width: 3px,
$focus-ring-radius: 10px
);

&:hover {
@include flashbar.notificationBar(
$color: palette.$text-on-accent,
$background: palette.$genai-hover,
$border-color: palette.$genai-hover
);
}

&:active {
@include flashbar.notificationBar(
$color: palette.$text-on-accent,
$background: palette.$genai-active,
$border-color: palette.$genai-active
);
}
}
62 changes: 62 additions & 0 deletions src/flashbar/__tests__/style-api.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React from 'react';
import { render } from '@testing-library/react';

import Flashbar, { FlashbarProps } from '../../../lib/components/flashbar';
import createWrapper from '../../../lib/components/test-utils/dom';

import styles from '../../../lib/components/flashbar/styles.css.js';

function renderFlashbar(props: FlashbarProps & { styleClassNames?: Record<string, unknown> }) {
const { container } = render(<Flashbar {...props} />);
return { container, wrapper: createWrapper(container).findFlashbar()! };
}

const noop = () => void 0;

describe('Flashbar Style API v2', () => {
test('applies each slot class to its element and ignores unknown keys', () => {
const { container, wrapper } = renderFlashbar({
items: [
{ type: 'success', content: 'Content', dismissible: true, dismissLabel: 'Dismiss', onDismiss: noop, id: '1' },
],
styleClassNames: { item: 'a', icon: 'b', dismissButton: 'c', unknown: 'd' },
});
const item = wrapper.findItems()[0];
expect(wrapper.findItemById('1')!.getElement()).toHaveClass('a');
expect(item.findByClassName(styles['flash-icon'])!.getElement()).toHaveClass('b');
expect(item.findDismissButton()!.getElement()).toHaveClass('c');
expect(container.querySelectorAll('.a, .b, .c')).toHaveLength(3);
expect(container.querySelector('.d')).toBeNull();
});

test('resolves the item callback for the full item and the collapsed placeholders of a stacked flashbar', () => {
const { container, wrapper } = renderFlashbar({
stackItems: true,
items: [
{ type: 'success', content: 'Content', id: '1' },
{ type: 'error', content: 'Content', id: '2' },
{ type: 'info', content: 'Content', id: '3' },
],
styleClassNames: {
item: ({ item }: { item: FlashbarProps.MessageDefinition }) => `item-${item.type}`,
notificationBar: 'bar',
},
});
expect(wrapper.findItemById('1')!.getElement()).toHaveClass('item-success');
const placeholders = container.querySelectorAll('li[aria-hidden="true"]');
expect(placeholders).toHaveLength(2);
expect(placeholders[0]).toHaveClass('item-error');
expect(placeholders[1]).toHaveClass('item-info');
expect(wrapper.findByClassName(styles['notification-bar'])!.getElement()).toHaveClass('bar');
});

test('does not leak the styleClassNames prop to the DOM', () => {
const { container } = renderFlashbar({
items: [{ type: 'info', content: 'Content', id: '1' }],
styleClassNames: { item: 'a' },
});
expect(container.querySelector('[styleClassNames]')).toBeNull();
});
});
14 changes: 12 additions & 2 deletions src/flashbar/collapsible-flashbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ const maxNonCollapsibleItems = 1;

const resizeListenerThrottleDelay = 100;

export default function CollapsibleFlashbar({ items, style, ...restProps }: InternalFlashbarProps) {
export default function CollapsibleFlashbar({ items, style, styleClassNames, ...restProps }: InternalFlashbarProps) {
const visibleItems = useFlashbarVisibility(items);
const [enteringItems, setEnteringItems] = useState<ReadonlyArray<FlashbarProps.MessageDefinition>>([]);
const [exitingItems, setExitingItems] = useState<ReadonlyArray<FlashbarProps.MessageDefinition>>([]);
Expand Down Expand Up @@ -228,6 +228,10 @@ export default function CollapsibleFlashbar({ items, style, ...restProps }: Inte

const getAnimationElementId = (item: StackableItem) => `flash-${getItemId(item)}`;

// Style API v2
const getItemClass = (item: FlashbarProps.MessageDefinition) =>
typeof styleClassNames?.item === 'function' ? styleClassNames.item({ item }) : styleClassNames?.item;

const renderList = () => (
<ul
ref={listElementRef}
Expand Down Expand Up @@ -272,7 +276,7 @@ export default function CollapsibleFlashbar({ items, style, ...restProps }: Inte
!isFlashbarStackExpanded && styles.item,
!collapsedItemRefs.current[getAnimationElementId(item)] && styles['expanded-only']
)
: clsx(styles.flash, styles[`flash-type-${item.type ?? 'info'}`], styles.item)
: clsx(getItemClass(item), styles.flash, styles[`flash-type-${item.type ?? 'info'}`], styles.item)
}
ref={element => {
if (isFlashbarStackExpanded) {
Expand Down Expand Up @@ -314,6 +318,11 @@ export default function CollapsibleFlashbar({ items, style, ...restProps }: Inte
i18nStrings={iconAriaLabels}
style={style}
onDismissed={handleFlashDismissed}
styleClassNames={{
item: getItemClass(item),
icon: styleClassNames?.icon,
dismissButton: styleClassNames?.dismissButton,
}}
{...item}
/>
)}
Expand Down Expand Up @@ -346,6 +355,7 @@ export default function CollapsibleFlashbar({ items, style, ...restProps }: Inte
{isCollapsible && (
<div
className={clsx(
styleClassNames?.notificationBar,
styles['notification-bar'],
isVisualRefresh && styles['visual-refresh'],
isFlashbarStackExpanded ? styles.expanded : styles.collapsed,
Expand Down
32 changes: 21 additions & 11 deletions src/flashbar/collapsible.scss
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,21 @@
@use '../internal/styles' as styles;
@use '../internal/styles/typography' as typography;
@use '../internal/styles/utils/theming' as theming;
@use '../internal/styles/style-api' as style-api;
@use './collapsible.motion';

// Style API v2. The focus-ring tokens are registered once, in styles.scss.
$_bar-tokens: style-api.resolve(style-api.$focus-ring, carrier);
@include style-api.docs(
'notificationBar',
$_bar-tokens,
$properties: (color, background, border, border-radius, box-shadow, padding),
$descriptions: (
slot:
'The bar under the stacked messages that expands and collapses the stack. Rendered for `stackItems` with more than one message; use `:hover` and `:active` for interaction states.'
)
);

$notification-bar-top-overlap: (
'collapsed': calc(#{awsui.$space-scaled-xl} + 3px),
'expanded': awsui.$space-scaled-s,
Expand Down Expand Up @@ -171,6 +184,7 @@ the grid layout will be:
}

.stack > .notification-bar {
@include style-api.carriers($_bar-tokens);
@include styles.text-wrapping;
background: var(#{custom-props.$styleBackgroundDefault}, awsui.$color-background-notification-stack-bar);
border-color: var(#{custom-props.$styleBorderColorDefault}, awsui.$color-border-notification-stack-bar);
Expand Down Expand Up @@ -203,7 +217,7 @@ the grid layout will be:
> .status > .item-count,
> .button {
@include typography.default-text-style;
color: awsui.$color-text-notification-stack-bar;
color: inherit;
cursor: pointer;
margin-block: 0;
padding-block: 0;
Expand Down Expand Up @@ -278,9 +292,6 @@ the grid layout will be:
@include theming.one-theme-only {
padding-inline: 0;
}
#{custom-props.$styleFocusRingBoxShadow}: 0 0 0
var(#{custom-props.$styleFocusRingBorderWidth}, foundation.$box-shadow-focused-width)
var(#{custom-props.$styleFocusRingBorderColor}, awsui.$color-border-notification-stack-bar-focused);

> .icon {
@include styles.with-motion {
Expand All @@ -296,13 +307,12 @@ the grid layout will be:
}

@include focus-visible.when-visible {
@include styles.focus-highlight(
$gutter: 0px,
$border-radius: var(
#{custom-props.$styleFocusRingBorderRadius},
awsui.$border-radius-control-default-focus-ring
),
$box-shadow: var(#{custom-props.$styleFocusRingBoxShadow})
@include style-api.focus-highlight(
$gutter-block: 0px,
$gutter-inline: 0px,
$radius: var(#{custom-props.$styleFocusRingBorderRadius}, awsui.$border-radius-control-default-focus-ring),
$width: var(#{custom-props.$styleFocusRingBorderWidth}, foundation.$box-shadow-focused-width),
$color: var(#{custom-props.$styleFocusRingBorderColor}, awsui.$color-border-notification-stack-bar-focused)
);
}
}
Expand Down
Loading
Loading