From 8a867c01582430a7bc04f45eb152902121557d20 Mon Sep 17 00:00:00 2001 From: Dylan Kilgore Date: Thu, 18 May 2023 15:13:15 -0700 Subject: [PATCH 1/4] feat: messagebar: add message bar component --- .../MessageBar/MessageBar.stories.tsx | 104 ++++++++ src/components/MessageBar/MessageBar.tsx | 119 +++++++++ src/components/MessageBar/MessageBar.types.ts | 21 ++ .../__snapshots__/messageBar.test.tsx.snap | 241 ++++++++++++++++++ src/components/MessageBar/index.ts | 2 + .../MessageBar/messageBar.module.scss | 64 +++++ src/components/MessageBar/messageBar.test.tsx | 111 ++++++++ src/styles/themes/_default-theme.scss | 14 + 8 files changed, 676 insertions(+) create mode 100644 src/components/MessageBar/MessageBar.stories.tsx create mode 100644 src/components/MessageBar/MessageBar.tsx create mode 100644 src/components/MessageBar/MessageBar.types.ts create mode 100644 src/components/MessageBar/__snapshots__/messageBar.test.tsx.snap create mode 100644 src/components/MessageBar/index.ts create mode 100644 src/components/MessageBar/messageBar.module.scss create mode 100644 src/components/MessageBar/messageBar.test.tsx diff --git a/src/components/MessageBar/MessageBar.stories.tsx b/src/components/MessageBar/MessageBar.stories.tsx new file mode 100644 index 000000000..5ef1913bb --- /dev/null +++ b/src/components/MessageBar/MessageBar.stories.tsx @@ -0,0 +1,104 @@ +import React from 'react'; +import { Stories } from '@storybook/addon-docs'; +import { ComponentStory, ComponentMeta } from '@storybook/react'; +import { MessageBar, MessageBarType } from '.'; +import { IconName } from '../Icon'; + +export default { + title: 'Message Bar', + parameters: { + docs: { + page: (): JSX.Element => ( +
+
+
+

Message Bar

+

+ These alret banners run across the whole viewport of the + interface. They are to be put at the top of the page to alert + the important message to the user. Depending on the severeity + and context of the alert, these banners can have close button on + the right and another CTA for the users to take action. +

+
+
+ +
+
+
+ ), + }, + }, +} as ComponentMeta; + +const Neutral_Story: ComponentStory = (args) => ( + +); + +export const Neutral = Neutral_Story.bind({}); + +const Positive_Story: ComponentStory = (args) => ( + +); + +export const Positive = Positive_Story.bind({}); + +const Warning_Story: ComponentStory = (args) => ( + +); + +export const Warning = Warning_Story.bind({}); + +const Disruptive_Story: ComponentStory = (args) => ( + +); + +export const Disruptive = Disruptive_Story.bind({}); + +const messageBarArgs: Object = { + actionButtonProps: { + ariaLabel: 'Action', + classNames: 'my-action-btn-class', + 'data-test-id': 'my-action-btn-test-id', + iconProps: null, + id: 'myActionButton', + text: 'Action', + }, + closable: true, + header: 'Header 4 used in this MessageBar', + content: + 'Body 2 which is at 16px font size is used here in the body section of the MessageBar. The body text can wrap to multiple lines and the buttons will be vertically centered.', + style: {}, + classNames: 'my-message-bar-class', + closeButtonProps: { + classNames: 'my-close-btn-class', + 'data-test-id': 'my-close-btn-test-id', + id: 'myCloseButton', + }, + closeIcon: IconName.mdiClose, + icon: IconName.mdiCheckCircle, + role: 'alert', + type: MessageBarType.positive, +}; + +Neutral.args = { + ...messageBarArgs, + icon: IconName.mdiInformation, + type: MessageBarType.neutral, +}; + +Positive.args = { + ...messageBarArgs, +}; + +Warning.args = { + ...messageBarArgs, + icon: IconName.mdiAlertCircle, + type: MessageBarType.warning, +}; + +Disruptive.args = { + ...messageBarArgs, + icon: IconName.mdiInformation, + type: MessageBarType.disruptive, +}; diff --git a/src/components/MessageBar/MessageBar.tsx b/src/components/MessageBar/MessageBar.tsx new file mode 100644 index 000000000..926bec05a --- /dev/null +++ b/src/components/MessageBar/MessageBar.tsx @@ -0,0 +1,119 @@ +import React, { FC, Ref, useEffect, useState } from 'react'; +import { MessageBarsProps, MessageBarType } from './MessageBar.types'; +import { InfoBarLocale } from '../InfoBar/InfoBar.types'; +import { Icon, IconName } from '../Icon'; +import { mergeClasses } from '../../shared/utilities'; +import { ButtonShape, NeutralButton } from '../Button'; +import LocaleReceiver, { + useLocaleReceiver, +} from '../LocaleProvider/LocaleReceiver'; +import enUS from '../InfoBar/Locale/en_US'; + +import styles from './messageBar.module.scss'; + +export const MessageBar: FC = React.forwardRef( + (props: MessageBarsProps, ref: Ref) => { + const { + actionButtonProps, + classNames, + closable, + closeButtonAriaLabelText: defaultCloseButtonAriaLabelText, + closeButtonProps, + closeIcon = IconName.mdiClose, + content, + header, + icon, + locale = enUS, + onClose, + role = 'alert', + style, + type = MessageBarType.neutral, + ...rest + } = props; + + // ============================ Strings =========================== + const [infoBarLocale] = useLocaleReceiver('InfoBar'); + let mergedLocale: InfoBarLocale; + + if (props.locale) { + mergedLocale = props.locale; + } else { + mergedLocale = infoBarLocale || props.locale; + } + + const [closeButtonAriaLabelText, setCloseButtonAriaLabelText] = + useState(defaultCloseButtonAriaLabelText); + + // Locs: if the prop isn't provided use the loc defaults. + // If the mergedLocale is changed, update. + useEffect(() => { + setCloseButtonAriaLabelText( + props.closeButtonAriaLabelText + ? props.closeButtonAriaLabelText + : mergedLocale.lang!.closeButtonAriaLabelText + ); + }, [mergedLocale]); + + const messageBarClassNames: string = mergeClasses([ + styles.messageBar, + classNames, + { [styles.neutral]: type === MessageBarType.neutral }, + { [styles.positive]: type === MessageBarType.positive }, + { [styles.warning]: type === MessageBarType.warning }, + { [styles.disruptive]: type === MessageBarType.disruptive }, + ]); + + const getIconName = (): IconName => { + if (icon) { + return icon; + } + switch (type) { + case MessageBarType.disruptive: + case MessageBarType.neutral: + return IconName.mdiInformation; + case MessageBarType.positive: + return IconName.mdiCheckCircle; + case MessageBarType.warning: + return IconName.mdiAlert; + } + }; + + return ( + + {(_contextLocale: InfoBarLocale) => { + return ( +
+ +
+ {header &&

{header}

} +
{content}
+
+ {(actionButtonProps || closable) && ( +
+ {actionButtonProps && ( + + )} + {closable && ( + + )} +
+ )} +
+ ); + }} +
+ ); + } +); diff --git a/src/components/MessageBar/MessageBar.types.ts b/src/components/MessageBar/MessageBar.types.ts new file mode 100644 index 000000000..e6e1cce80 --- /dev/null +++ b/src/components/MessageBar/MessageBar.types.ts @@ -0,0 +1,21 @@ +import React from 'react'; +import { InfoBarsProps } from '../InfoBar'; + +export enum MessageBarType { + neutral = 'neutral', + positive = 'positive', + warning = 'warning', + disruptive = 'disruptive', +} + +export interface MessageBarsProps extends Omit { + /** + * Header of the MessageBar + */ + header?: React.ReactNode; + /** + * Type of the MessageBar + * @default MessageBarType.neutral + */ + type?: MessageBarType; +} diff --git a/src/components/MessageBar/__snapshots__/messageBar.test.tsx.snap b/src/components/MessageBar/__snapshots__/messageBar.test.tsx.snap new file mode 100644 index 000000000..21e9619d8 --- /dev/null +++ b/src/components/MessageBar/__snapshots__/messageBar.test.tsx.snap @@ -0,0 +1,241 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`MessageBar MessageBar is Disruptive 1`] = ` +
+ +
+`; + +exports[`MessageBar MessageBar is Neutral 1`] = ` +
+ +
+`; + +exports[`MessageBar MessageBar is Positive 1`] = ` +
+ +
+`; + +exports[`MessageBar MessageBar is Warning 1`] = ` +
+ +
+`; + +exports[`MessageBar Renders a custom icon when the icon prop uses a custom icon 1`] = ` +
+ +
+`; + +exports[`MessageBar Renders without crashing 1`] = ` +
+ +
+`; diff --git a/src/components/MessageBar/index.ts b/src/components/MessageBar/index.ts new file mode 100644 index 000000000..58de50093 --- /dev/null +++ b/src/components/MessageBar/index.ts @@ -0,0 +1,2 @@ +export * from './MessageBar'; +export * from './MessageBar.types'; diff --git a/src/components/MessageBar/messageBar.module.scss b/src/components/MessageBar/messageBar.module.scss new file mode 100644 index 000000000..fe77a0847 --- /dev/null +++ b/src/components/MessageBar/messageBar.module.scss @@ -0,0 +1,64 @@ +.message-bar { + align-items: start; + background-color: var(--message-bar-background-color); + border-radius: var(--message-bar-border-radius); + border-left: solid $space-xs var(--message-bar-neutral-color); + box-shadow: var(--message-bar-box-shadow); + color: var(--message-bar-neutral-color); + display: flex; + flex-direction: row; + font-family: var(--font-stack-full); + gap: $space-xs; + padding: $space-ml; + width: 100%; + + &.neutral { + border-left: solid $space-xs var(--message-bar-neutral-color); + color: var(--message-bar-neutral-color); + } + + &.positive { + border-left: solid $space-xs var(--message-bar-positive-color); + color: var(--message-bar-positive-color); + } + + &.warning { + border-left: solid $space-xs var(--message-bar-warning-color); + color: var(--message-bar-warning-color); + } + + &.disruptive { + border-left: solid $space-xs var(--message-bar-disruptive-color); + color: var(--message-bar-disruptive-color); + } + + .icon { + color: inherit; + padding-top: $space-xxxs; + } + + .message { + align-items: flex-start; + display: flex; + flex: 1; + flex-direction: column; + gap: $space-xs; + + .header { + @include octuple-h4(); + color: inherit; + padding-bottom: 0; + padding-top: 0; + } + .inner-message { + color: var(--message-bar-text-color); + } + } + + .actions { + align-self: center; + display: flex; + flex-direction: row; + gap: $space-m; + } +} diff --git a/src/components/MessageBar/messageBar.test.tsx b/src/components/MessageBar/messageBar.test.tsx new file mode 100644 index 000000000..8aaca294d --- /dev/null +++ b/src/components/MessageBar/messageBar.test.tsx @@ -0,0 +1,111 @@ +import React from 'react'; +import Enzyme from 'enzyme'; +import Adapter from '@wojtekmaj/enzyme-adapter-react-17'; +import MatchMediaMock from 'jest-matchmedia-mock'; +import { MessageBar, MessageBarType } from '.'; +import { fireEvent, render } from '@testing-library/react'; +import { IconName } from '../Icon'; + +Enzyme.configure({ adapter: new Adapter() }); + +let matchMedia: any; + +describe('MessageBar', () => { + beforeAll(() => { + matchMedia = new MatchMediaMock(); + }); + + afterEach(() => { + matchMedia.clear(); + }); + + test('Renders without crashing', () => { + const { container, getByRole } = render( + + ); + const infoBar = getByRole('alert'); + expect(() => container).not.toThrowError(); + expect(infoBar).toBeTruthy(); + expect(container).toMatchSnapshot(); + }); + + test('Calls onClose callback when close button is clicked', () => { + const onClose = jest.fn(); + const { container } = render( + + ); + fireEvent.click(container.querySelector('.close-button')); + expect(onClose).toHaveBeenCalled(); + }); + + test('Renders a custom icon when the icon prop uses a custom icon', () => { + const { container } = render( + + ); + expect(container.querySelector('.icon')).toBeTruthy(); + expect(container).toMatchSnapshot(); + }); + + test('MessageBar is Disruptive', () => { + const { container } = render( + + ); + expect(container.querySelector('.disruptive')).toBeTruthy(); + expect(container).toMatchSnapshot(); + }); + + test('MessageBar is Neutral', () => { + const { container } = render( + + ); + expect(container.querySelector('.neutral')).toBeTruthy(); + expect(container).toMatchSnapshot(); + }); + + test('MessageBar is Positive', () => { + const { container } = render( + + ); + expect(container.querySelector('.positive')).toBeTruthy(); + expect(container).toMatchSnapshot(); + }); + + test('MessageBar is Warning', () => { + const { container } = render( + + ); + expect(container.querySelector('.warning')).toBeTruthy(); + expect(container).toMatchSnapshot(); + }); +}); diff --git a/src/styles/themes/_default-theme.scss b/src/styles/themes/_default-theme.scss index 50a6e5325..65f633d66 100644 --- a/src/styles/themes/_default-theme.scss +++ b/src/styles/themes/_default-theme.scss @@ -830,10 +830,24 @@ --accordion-summary-text-color: var(--text-primary-color); --accordion-summary-text-hover-color: var(--text-primary-color); --accordion-pill-shape-border-radius: var(--border-radius-xl); + // ------ Accordion theme ------ // ------ Empty theme ------ --empty-svg-accent-20-color: var(--accent-color-20); --empty-svg-primary-10-color: var(--primary-color-10); --empty-svg-primary-30-color: var(--primary-color-30); --empty-svg-white-color: var(--white-color); + // ------ Empty theme ------ + + // ------ Message Bar theme ------ + --message-bar-background-color: var(--background-color); + --message-bar-border-radius: var(--border-radius-xs); + --message-bar-box-shadow: 0px -1px 2px rgba(15, 20, 31, 0.12), + 0px 4px 16px rgba(15, 20, 31, 0.16); + --message-bar-disruptive-color: var(--disruptive-color-70); + --message-bar-neutral-color: var(--grey-color-70); + --message-bar-positive-color: var(--green-color-70); + --message-bar-text-color: var(--text-primary-color); + --message-bar-warning-color: var(--orange-color-70); + // ------ Message Bar theme ------ } From 3a7a9b0f1f49b60f5707056e5494469b5ff7fc48 Mon Sep 17 00:00:00 2001 From: Dylan Kilgore Date: Thu, 18 May 2023 15:47:45 -0700 Subject: [PATCH 2/4] chore: messagebar: update icons per spec --- src/components/MessageBar/MessageBar.stories.tsx | 4 ++-- src/components/MessageBar/MessageBar.tsx | 1 + .../MessageBar/__snapshots__/messageBar.test.tsx.snap | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/MessageBar/MessageBar.stories.tsx b/src/components/MessageBar/MessageBar.stories.tsx index 5ef1913bb..35191fe19 100644 --- a/src/components/MessageBar/MessageBar.stories.tsx +++ b/src/components/MessageBar/MessageBar.stories.tsx @@ -93,12 +93,12 @@ Positive.args = { Warning.args = { ...messageBarArgs, - icon: IconName.mdiAlertCircle, + icon: IconName.mdiAlert, type: MessageBarType.warning, }; Disruptive.args = { ...messageBarArgs, - icon: IconName.mdiInformation, + icon: IconName.mdiAlertCircle, type: MessageBarType.disruptive, }; diff --git a/src/components/MessageBar/MessageBar.tsx b/src/components/MessageBar/MessageBar.tsx index 926bec05a..7758cff68 100644 --- a/src/components/MessageBar/MessageBar.tsx +++ b/src/components/MessageBar/MessageBar.tsx @@ -69,6 +69,7 @@ export const MessageBar: FC = React.forwardRef( } switch (type) { case MessageBarType.disruptive: + return IconName.mdiAlertCircle; case MessageBarType.neutral: return IconName.mdiInformation; case MessageBarType.positive: diff --git a/src/components/MessageBar/__snapshots__/messageBar.test.tsx.snap b/src/components/MessageBar/__snapshots__/messageBar.test.tsx.snap index 21e9619d8..41fef7473 100644 --- a/src/components/MessageBar/__snapshots__/messageBar.test.tsx.snap +++ b/src/components/MessageBar/__snapshots__/messageBar.test.tsx.snap @@ -17,7 +17,7 @@ exports[`MessageBar MessageBar is Disruptive 1`] = ` viewBox="0 0 24 24" > From daafa79faad09d526ad29cfc35922b3488aab1e8 Mon Sep 17 00:00:00 2001 From: Dylan Kilgore Date: Thu, 18 May 2023 16:12:29 -0700 Subject: [PATCH 3/4] chore: messagebar: export component --- src/octuple.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/octuple.ts b/src/octuple.ts index 4df71542a..b2c96cfe0 100644 --- a/src/octuple.ts +++ b/src/octuple.ts @@ -94,6 +94,8 @@ import { import { List } from './components/List'; +import { MessageBar, MessageBarType } from './components/MessageBar'; + import { CascadingMenu, Menu, @@ -316,6 +318,8 @@ export { MenuItemType, MenuVariant, MenuSize, + MessageBar, + MessageBarType, Modal, ModalSize, Navbar, From 513fbd4353d3fae93b4becafbae86f703b304f91 Mon Sep 17 00:00:00 2001 From: Dylan Kilgore Date: Mon, 22 May 2023 08:57:11 -0700 Subject: [PATCH 4/4] chore: messagebar: address pr feedback by adding a mapping --- src/components/MessageBar/MessageBar.tsx | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/components/MessageBar/MessageBar.tsx b/src/components/MessageBar/MessageBar.tsx index 7758cff68..d2a4f9e08 100644 --- a/src/components/MessageBar/MessageBar.tsx +++ b/src/components/MessageBar/MessageBar.tsx @@ -63,20 +63,18 @@ export const MessageBar: FC = React.forwardRef( { [styles.disruptive]: type === MessageBarType.disruptive }, ]); + const messageBarTypeToIconNameMap = new Map([ + [MessageBarType.disruptive, IconName.mdiAlertCircle], + [MessageBarType.neutral, IconName.mdiInformation], + [MessageBarType.positive, IconName.mdiCheckCircle], + [MessageBarType.warning, IconName.mdiAlert], + ]); + const getIconName = (): IconName => { if (icon) { return icon; } - switch (type) { - case MessageBarType.disruptive: - return IconName.mdiAlertCircle; - case MessageBarType.neutral: - return IconName.mdiInformation; - case MessageBarType.positive: - return IconName.mdiCheckCircle; - case MessageBarType.warning: - return IconName.mdiAlert; - } + return messageBarTypeToIconNameMap.get(type); }; return (