From f0db23b7778683065cd858a8cf673228ce02b1a8 Mon Sep 17 00:00:00 2001 From: Ryan Merrill Date: Thu, 13 Jun 2024 17:24:40 -0400 Subject: [PATCH 01/34] Adds MessageBar component to FluentUI --- ...-6860ddb3-4e1b-4679-9c94-1dfcf4bf40ca.json | 7 + packages/web-components/package.json | 4 + packages/web-components/src/index-rollup.ts | 1 + .../web-components/src/message-bar/define.ts | 4 + .../web-components/src/message-bar/index.ts | 5 + .../src/message-bar/message-bar.definition.ts | 20 +++ .../message-bar.integration.spec.ts | 83 +++++++++ .../src/message-bar/message-bar.options.ts | 47 ++++++ .../src/message-bar/message-bar.stories.ts | 158 ++++++++++++++++++ .../src/message-bar/message-bar.styles.ts | 132 +++++++++++++++ .../src/message-bar/message-bar.template.ts | 77 +++++++++ .../src/message-bar/message-bar.ts | 70 ++++++++ 12 files changed, 608 insertions(+) create mode 100644 change/@fluentui-web-components-6860ddb3-4e1b-4679-9c94-1dfcf4bf40ca.json create mode 100644 packages/web-components/src/message-bar/define.ts create mode 100644 packages/web-components/src/message-bar/index.ts create mode 100644 packages/web-components/src/message-bar/message-bar.definition.ts create mode 100644 packages/web-components/src/message-bar/message-bar.integration.spec.ts create mode 100644 packages/web-components/src/message-bar/message-bar.options.ts create mode 100644 packages/web-components/src/message-bar/message-bar.stories.ts create mode 100644 packages/web-components/src/message-bar/message-bar.styles.ts create mode 100644 packages/web-components/src/message-bar/message-bar.template.ts create mode 100644 packages/web-components/src/message-bar/message-bar.ts diff --git a/change/@fluentui-web-components-6860ddb3-4e1b-4679-9c94-1dfcf4bf40ca.json b/change/@fluentui-web-components-6860ddb3-4e1b-4679-9c94-1dfcf4bf40ca.json new file mode 100644 index 0000000000000..4510f8e474210 --- /dev/null +++ b/change/@fluentui-web-components-6860ddb3-4e1b-4679-9c94-1dfcf4bf40ca.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "Adds MessageBar component to Fluent Web Components", + "packageName": "@fluentui/web-components", + "email": "ryan@ryanmerrill.net", + "dependentChangeType": "patch" +} diff --git a/packages/web-components/package.json b/packages/web-components/package.json index 0cebe6ce78642..9e9797299f180 100644 --- a/packages/web-components/package.json +++ b/packages/web-components/package.json @@ -106,6 +106,10 @@ "types": "./dist/dts/menu-item/define.d.ts", "default": "./dist/esm/menu-item/define.js" }, + "./message-bar.js": { + "types": "./dist/dts/message-bar/define.d.ts", + "default": "./dist/esm/message-bar/define.js" + }, "./progress-bar.js": { "types": "./dist/dts/progress-bar/define.d.ts", "default": "./dist/esm/progress-bar/define.js" diff --git a/packages/web-components/src/index-rollup.ts b/packages/web-components/src/index-rollup.ts index a56cc040003ff..1e9e779465956 100644 --- a/packages/web-components/src/index-rollup.ts +++ b/packages/web-components/src/index-rollup.ts @@ -18,6 +18,7 @@ import './menu-button/define.js'; import './menu-item/define.js'; import './menu-list/define.js'; import './menu/define.js'; +import './message-bar/define.js'; import './progress-bar/define.js'; import './radio-group/define.js'; import './radio/define.js'; diff --git a/packages/web-components/src/message-bar/define.ts b/packages/web-components/src/message-bar/define.ts new file mode 100644 index 0000000000000..ef07193254fd2 --- /dev/null +++ b/packages/web-components/src/message-bar/define.ts @@ -0,0 +1,4 @@ +import { FluentDesignSystem } from '../fluent-design-system.js'; +import { definition } from './message-bar.definition.js'; + +definition.define(FluentDesignSystem.registry); diff --git a/packages/web-components/src/message-bar/index.ts b/packages/web-components/src/message-bar/index.ts new file mode 100644 index 0000000000000..37ea124ca6325 --- /dev/null +++ b/packages/web-components/src/message-bar/index.ts @@ -0,0 +1,5 @@ +export { definition as MessageBarDefinition } from './message-bar.definition.js'; +export { MessageBar } from './message-bar.js'; +export { MessageBarIntent, MessageBarLayout, MessageBarPoliteness, MessageBarShape } from './message-bar.options.js'; +export { styles as MessageBarStyles } from './message-bar.styles.js'; +export { template as MessageBarTemplate } from './message-bar.template.js'; diff --git a/packages/web-components/src/message-bar/message-bar.definition.ts b/packages/web-components/src/message-bar/message-bar.definition.ts new file mode 100644 index 0000000000000..e58c7c39e231d --- /dev/null +++ b/packages/web-components/src/message-bar/message-bar.definition.ts @@ -0,0 +1,20 @@ +import { FluentDesignSystem } from '../fluent-design-system.js'; +import { MessageBar } from './message-bar.js'; +import { styles } from './message-bar.styles.js'; +import { template } from './message-bar.template.js'; + +/** + * The Fluent MessageBar Element definition. + * + * @public + * @remarks + * HTML Element: `` + */ +export const definition = MessageBar.compose({ + name: `${FluentDesignSystem.prefix}-message-bar`, + template, + styles, + shadowOptions: { + mode: FluentDesignSystem.shadowRootMode, + }, +}); diff --git a/packages/web-components/src/message-bar/message-bar.integration.spec.ts b/packages/web-components/src/message-bar/message-bar.integration.spec.ts new file mode 100644 index 0000000000000..b9cbce6cacf8f --- /dev/null +++ b/packages/web-components/src/message-bar/message-bar.integration.spec.ts @@ -0,0 +1,83 @@ +import { expect, test } from '@playwright/test'; +import type { Locator, Page } from '@playwright/test'; + +test.describe('MessageBar component', () => { + const componentID = 'components-messagebar--message-bar'; + let page: Page; + let element: Locator; + + test.beforeAll(async ({ browser }) => { + const context = await browser.newContext(); + page = await context.newPage(); + root = page.locator('#root'); + element = page.locator('fluent-message-bar'); + await page.goto(`iframe.html?id=${componentID}`); + }); + + test.afterAll(async () => { + await page.close(); + }); + + test('should render with default attributes', async () => { + const role = await element.getAttribute('role'); + const shape = await element.getAttribute('shape'); + const layout = await element.getAttribute('layout'); + const intent = await element.getAttribute('intent'); + const politeness = await element.getAttribute('aria-live'); + + expect(role).toEqual('status'); + expect(shape).toEqual('rounded'); + expect(layout).toEqual('singleline'); + expect(intent).toEqual('info'); + expect(politeness).toEqual('polite'); + }); + + test('should have correct `aria-labelledby` attribute', async () => { + const attribute = await element.getAttribute('aria-labelledby'); + expect(attribute).toBeNull(); + }); + + test('on dismissing the MessageBar, it should emit `dismiss` event', async () => { + const [dismissed] = await Promise.all([ + element.evaluate(el => { + return new Promise(resolve => { + el.addEventListener('dismiss', () => resolve(true), { + once: true, + }); + (el as any).dismissMessageBar(); + }); + }), + ]); + expect(dismissed).toBeTruthy(); + }); + + test('when the `layout` attribute is changed, it should reflect the new value', async () => { + await element.evaluate(el => el.setAttribute('layout', 'multiline')); + const layout = await element.getAttribute('layout'); + expect(layout).toEqual('multiline'); + }); + + test('when the `intent` attribute is changed, it should reflect the new value', async () => { + await element.evaluate(el => el.setAttribute('intent', 'warning')); + const intent = await element.getAttribute('intent'); + expect(intent).toEqual('warning'); + }); + + test('when the `shape` attribute is changed, it should reflect the new value', async () => { + await element.evaluate(el => el.setAttribute('shape', 'rectangular')); + const shape = await element.getAttribute('shape'); + expect(shape).toEqual('rectangular'); + }); + + test('when the `politeness` attribute is changed, it should reflect the new value', async () => { + await element.evaluate(el => el.setAttribute('politeness', 'polite')); + const politeness = await element.getAttribute('politeness'); + expect(politeness).toEqual('polite'); + }); + + test('when the `politeness` attribute is set, it should reflect the value in the aria-live attribute', async () => { + await element.evaluate(el => el.setAttribute('politeness', 'assertive')); + const ariaLive = await element.getAttribute('aria-live'); + expect(ariaLive).toEqual('assertive'); + }); +}); diff --git a/packages/web-components/src/message-bar/message-bar.options.ts b/packages/web-components/src/message-bar/message-bar.options.ts new file mode 100644 index 0000000000000..be04893203b71 --- /dev/null +++ b/packages/web-components/src/message-bar/message-bar.options.ts @@ -0,0 +1,47 @@ +import type { ValuesOf } from '../utils/typings.js'; + +/** + * @public + * The `layout` variations for the MessageBar component. + */ +export const MessageBarLayout = { + multiline: 'multiline', + singleline: 'singleline', +} as const; + +export type MessageBarLayout = ValuesOf; + +/** + * @public + * The `shape` variations for the MessageBar component. + */ +export const MessageBarShape = { + rounded: 'rounded', + square: 'square', +} as const; + +export type MessageBarShape = ValuesOf; + +/** + * @public + * The `intent` variations for the MessageBar component. + */ +export const MessageBarIntent = { + success: 'success', + warning: 'warning', + error: 'error', + info: 'info', +} as const; + +export type MessageBarIntent = ValuesOf; + +/** + * @public + * The `politeness` variations for the MessageBar component. + */ +export const MessageBarPoliteness = { + polite: 'polite', + assertive: 'assertive', +} as const; + +export type MessageBarPoliteness = ValuesOf; diff --git a/packages/web-components/src/message-bar/message-bar.stories.ts b/packages/web-components/src/message-bar/message-bar.stories.ts new file mode 100644 index 0000000000000..6d50cae16b9ed --- /dev/null +++ b/packages/web-components/src/message-bar/message-bar.stories.ts @@ -0,0 +1,158 @@ +import { html } from '@microsoft/fast-element'; +import type { Args, Meta } from '@storybook/html'; +import { renderComponent } from '../helpers.stories.js'; +import { MessageBar as FluentMessageBar } from './message-bar.js'; +import { MessageBarIntent, MessageBarLayout, MessageBarPoliteness, MessageBarShape } from './message-bar.options.js'; +import './define'; + +type MessageBarStoryArgs = Args & FluentMessageBar; +type MessageBarStoryMeta = Meta; + +const dismissed20Regular = html` + +`; + +const storyTemplate = html` + + ${x => x.content} +
+ Action + Action +
+ ${dismissed20Regular} +
+`; + +export default { + title: 'Components/MessageBar', + args: { + content: 'This is a message bar that provides information to the user.', + shape: MessageBarShape.rounded, + layout: MessageBarLayout.singleline, + intent: MessageBarIntent.info, + politeness: MessageBarPoliteness.polite, + }, + argTypes: { + content: { + description: 'MessageBar content', + control: { type: 'text' }, + }, + shape: { + description: 'MessageBar shape', + control: { type: 'select' }, + options: Object.values(MessageBarShape), + }, + layout: { + description: 'MessageBar layout', + control: { type: 'select' }, + options: Object.values(MessageBarLayout), + }, + intent: { + description: 'MessageBar intent', + control: { type: 'select' }, + options: Object.values(MessageBarIntent), + }, + politeness: { + description: 'MessageBar politeness', + control: { type: 'select' }, + options: Object.values(MessageBarPoliteness), + }, + }, +} as MessageBarStoryMeta; + +export const MessageBar = renderComponent(storyTemplate).bind({}) as any; + +export const Shape = renderComponent(html` + + rounded +
+ Action + Action +
+ ${dismissed20Regular} +
+
+ + square +
+ Action + Action +
+ ${dismissed20Regular} +
+`); + +export const Layout = renderComponent(html` + + singleline +
+ Action + Action +
+ ${dismissed20Regular} +
+
+ + multiline +
+ Action + Action +
+ ${dismissed20Regular} +
+`); + +export const Intent = renderComponent(html` + + info +
+ Action + Action +
+ ${dismissed20Regular} +
+
+ + warning +
+ Action + Action +
+ ${dismissed20Regular} +
+
+ + success +
+ Action + Action +
+ ${dismissed20Regular} +
+
+ + error +
+ Action + Action +
+ ${dismissed20Regular} +
+`); diff --git a/packages/web-components/src/message-bar/message-bar.styles.ts b/packages/web-components/src/message-bar/message-bar.styles.ts new file mode 100644 index 0000000000000..231c545a8e59a --- /dev/null +++ b/packages/web-components/src/message-bar/message-bar.styles.ts @@ -0,0 +1,132 @@ +import type { ElementStyles } from '@microsoft/fast-element'; +import { css } from '@microsoft/fast-element'; +import { + borderRadiusMedium, + colorNeutralBackground3, + colorNeutralForeground3, + colorNeutralStroke1, + colorPaletteDarkOrangeBackground1, + colorPaletteDarkOrangeBorder1, + colorPaletteDarkOrangeForeground1, + colorPaletteGreenBackground1, + colorPaletteGreenBorder1, + colorPaletteGreenForeground1, + colorPaletteRedBackground1, + colorPaletteRedBorder1, + colorPaletteRedForeground1, + fontFamilyBase, + fontSizeBase200, + lineHeightBase200, + spacingHorizontalM, + spacingHorizontalS, + spacingVerticalMNudge, +} from '../theme/design-tokens.js'; + +/** + * Styles for the MessageBar component. + * + * @public + */ +export const styles: ElementStyles = css` + :host { + display: grid; + box-sizing: border-box; + font-family: ${fontFamilyBase}; + font-size: ${fontSizeBase200}; + line-height: ${lineHeightBase200}; + width: 100%; + background: ${colorNeutralBackground3}; + border: 1px solid ${colorNeutralStroke1}; + padding: 0 ${spacingHorizontalM}; + border-radius: ${borderRadiusMedium}; + min-height: 36px; + align-items: center; + grid-template: 'icon body actions close' / auto 1fr auto auto; + } + + :host([shape='square']) { + border-radius: 0; + } + + :host([intent='success']) { + background-color: ${colorPaletteGreenBackground1}; + border-color: ${colorPaletteGreenBorder1}; + } + + :host([intent='warning']) { + background-color: ${colorPaletteDarkOrangeBackground1}; + border-color: ${colorPaletteDarkOrangeBorder1}; + } + + :host([intent='error']) { + background-color: ${colorPaletteRedBackground1}; + border-color: ${colorPaletteRedBorder1}; + } + + :host([layout='multiline']) { + grid-template-areas: + 'icon body close' + 'actions actions actions'; + grid-template-columns: auto 1fr auto; + grid-template-rows: auto auto auto; + padding: ${spacingVerticalMNudge} ${spacingHorizontalM}; + } + + .icon { + grid-area: icon; + display: flex; + flex-direction: column; + align-items: center; + color: ${colorNeutralForeground3}; + margin-right: ${spacingHorizontalS}; + } + + :host([intent='success']) .icon { + color: ${colorPaletteGreenForeground1}; + } + + :host([intent='warning']) .icon { + color: ${colorPaletteDarkOrangeForeground1}; + } + + :host([intent='error']) .icon { + color: ${colorPaletteRedForeground1}; + } + + .content { + max-width: 520px; + grid-area: body; + padding: ${spacingVerticalMNudge} 0; + } + + :host([layout='multiline']) .content { + padding: 0; + } + + .close { + grid-area: close; + display: flex; + justify-content: end; + } + + :host([layout='multiline']) .close { + flex-direction: column; + justify-content: start; + align-items: start; + } + + .actions { + grid-area: actions; + display: flex; + justify-content: end; + margin-right: ${spacingHorizontalS}; + } + + :host([layout='multiline']) .actions { + margin-top: ${spacingVerticalMNudge}; + } + + ::slotted(*) { + font-size: unset; + } +`; diff --git a/packages/web-components/src/message-bar/message-bar.template.ts b/packages/web-components/src/message-bar/message-bar.template.ts new file mode 100644 index 0000000000000..a0e19de215b09 --- /dev/null +++ b/packages/web-components/src/message-bar/message-bar.template.ts @@ -0,0 +1,77 @@ +import { ElementViewTemplate, html, when } from '@microsoft/fast-element'; +import type { MessageBar } from './message-bar.js'; + +const infoIcon = html` + + + +`; + +const warningIcon = html` + + + +`; + +const successIcon = html` + + + +`; + +const errorIcon = html` + + + +`; + +/** + * Generates a template for the MessageBar component. + * @public + * @param {MessageBar} T - The type of the MessageBar. + * @returns {ElementViewTemplate} - The template for the MessageBar component. + */ +export function messageBarTemplate(): ElementViewTemplate { + return html` + + `; +} + +/** + * The template for the MessageBar component. + * @type {ElementViewTemplate} + */ +export const template: ElementViewTemplate = messageBarTemplate(); diff --git a/packages/web-components/src/message-bar/message-bar.ts b/packages/web-components/src/message-bar/message-bar.ts new file mode 100644 index 0000000000000..7847732ad9e1e --- /dev/null +++ b/packages/web-components/src/message-bar/message-bar.ts @@ -0,0 +1,70 @@ +import { attr, FASTElement } from '@microsoft/fast-element'; +import { MessageBarIntent, MessageBarLayout, MessageBarPoliteness, MessageBarShape } from './message-bar.options.js'; + +/** + * A Message Bar Custom HTML Element. + * + * @slot actions - Content that can be provided for the actions + * @slot close - Content that can be provided for the close button + * @slot - The default slot for the content + * @public + */ +export class MessageBar extends FASTElement { + /** + * Sets the aria-labelledby attribute of the control. + * + * @public + * @remarks + * HTML Attribute: `aria-labelledby` + */ + @attr({ attribute: 'aria-labelledby' }) + ariaLabelledBy: string | null = null; + + /** + * Sets the shape of the control. + * + * @public + * @remarks + * HTML Attribute: `shape` + */ + @attr + shape: MessageBarShape = 'rounded'; + + /** + * Sets the layout of the control. + * + * @public + * @remarks + * HTML Attribute: `layout` + */ + @attr + layout: MessageBarLayout = 'singleline'; + + /** + * Sets the intent of the control. + * + * @public + * @remarks + * HTML Attribute: `intent` + */ + @attr + intent: MessageBarIntent = 'info'; + + /** + * Sets the politeness of the control. + * + * @public + * @remarks + * HTML Attribute: `politeness` + */ + @attr + politeness: MessageBarPoliteness = 'polite'; + + /** + * @public + * Method to emit a `dismiss` event when the message bar is dismissed + */ + public dismissMessageBar = () => { + this.$emit('dismiss', {}); + }; +} From cc0a9dff508ec13d24f247f9ce8e807ad8437701 Mon Sep 17 00:00:00 2001 From: Ryan Merrill Date: Fri, 14 Jun 2024 15:07:54 -0400 Subject: [PATCH 02/34] Udpates slots for rendering status icons in MessageBar --- .../src/message-bar/message-bar.styles.ts | 20 ++++-- .../src/message-bar/message-bar.template.ts | 72 ++++++++++++------- .../src/message-bar/message-bar.ts | 25 +++++++ 3 files changed, 85 insertions(+), 32 deletions(-) diff --git a/packages/web-components/src/message-bar/message-bar.styles.ts b/packages/web-components/src/message-bar/message-bar.styles.ts index 231c545a8e59a..ff72e7658f7eb 100644 --- a/packages/web-components/src/message-bar/message-bar.styles.ts +++ b/packages/web-components/src/message-bar/message-bar.styles.ts @@ -72,24 +72,34 @@ export const styles: ElementStyles = css` padding: ${spacingVerticalMNudge} ${spacingHorizontalM}; } - .icon { + .info, + .warning, + .error, + .success { + display: none; grid-area: icon; - display: flex; flex-direction: column; align-items: center; color: ${colorNeutralForeground3}; margin-right: ${spacingHorizontalS}; } - :host([intent='success']) .icon { + :host([intent='info']) .info, + :host([intent='warning']) .warning, + :host([intent='error']) .error, + :host([intent='success']) .success { + display: flex; + } + + :host([intent='success']) .success { color: ${colorPaletteGreenForeground1}; } - :host([intent='warning']) .icon { + :host([intent='warning']) .warning { color: ${colorPaletteDarkOrangeForeground1}; } - :host([intent='error']) .icon { + :host([intent='error']) .error { color: ${colorPaletteRedForeground1}; } diff --git a/packages/web-components/src/message-bar/message-bar.template.ts b/packages/web-components/src/message-bar/message-bar.template.ts index a0e19de215b09..be130e094da6e 100644 --- a/packages/web-components/src/message-bar/message-bar.template.ts +++ b/packages/web-components/src/message-bar/message-bar.template.ts @@ -1,41 +1,38 @@ import { ElementViewTemplate, html, when } from '@microsoft/fast-element'; -import type { MessageBar } from './message-bar.js'; +import { staticallyCompose } from '../utils/index.js'; +import type { MessageBar, MessageBarOptions } from './message-bar.js'; -const infoIcon = html` - +const infoIcon = + html.partial(` - -`; + `); -const warningIcon = html` - +const warningIcon = + html.partial(` - -`; + `); -const successIcon = html` - +const successIcon = + html.partial(` - -`; + `); -const errorIcon = html` - +const errorIcon = + html.partial(` - -`; + `); /** * Generates a template for the MessageBar component. @@ -43,19 +40,35 @@ const errorIcon = html` * @param {MessageBar} T - The type of the MessageBar. * @returns {ElementViewTemplate} - The template for the MessageBar component. */ -export function messageBarTemplate(): ElementViewTemplate { +export function messageBarTemplate(options: MessageBarOptions): ElementViewTemplate { return html`