From 1b44481164cd1312c901a3ecac20ad79f7f81743 Mon Sep 17 00:00:00 2001 From: Anna Wen <54281166+annawen1@users.noreply.github.com> Date: Tue, 18 Apr 2023 15:48:35 -0400 Subject: [PATCH] feat(notifications): sync with @carbon/react v11 (#10338) * feat(notifications): notifications v11 carbon sync * chore(slider): fix slider spec test * feat(notifications): new actionable variant * feat(notification): additional functionality * chore(notifications): add license to helper file * chore(notifications): update docs * feat(notification): warning icon fill and storybook adjustments --- .../codesandbox/form/redux-form/src/index.js | 4 +- .../actionable-notification-button.ts | 34 ++++ .../actionable-notification-story.ts | 120 ++++++++++++ .../notification/actionable-notification.scss | 170 ++++++++++++++++ .../notification/actionable-notification.ts | 184 ++++++++++++++++++ .../src/components/notification/defs.ts | 19 ++ .../src/components/notification/index.ts | 2 + .../notification/inline-notification-story.ts | 103 ++++++++++ .../notification/inline-notification.scss | 26 +++ .../notification/inline-notification.ts | 45 +++-- .../notification/notification-story.mdx | 29 +++ .../components/notification/stories/helper.ts | 22 +++ ...n-story.ts => toast-notification-story.ts} | 126 ++++-------- .../notification/toast-notification.scss | 26 +++ .../notification/toast-notification.ts | 8 +- .../carbon-web-components/src/index.ts | 2 + .../src/typings/jsx-elements.d.ts | 2 + .../tests/spec/notification_spec.ts | 12 +- .../tests/spec/slider_spec.ts | 4 +- 19 files changed, 821 insertions(+), 117 deletions(-) create mode 100644 web-components/packages/carbon-web-components/src/components/notification/actionable-notification-button.ts create mode 100644 web-components/packages/carbon-web-components/src/components/notification/actionable-notification-story.ts create mode 100644 web-components/packages/carbon-web-components/src/components/notification/actionable-notification.scss create mode 100644 web-components/packages/carbon-web-components/src/components/notification/actionable-notification.ts create mode 100644 web-components/packages/carbon-web-components/src/components/notification/inline-notification-story.ts create mode 100644 web-components/packages/carbon-web-components/src/components/notification/stories/helper.ts rename web-components/packages/carbon-web-components/src/components/notification/{notification-story.ts => toast-notification-story.ts} (50%) diff --git a/web-components/packages/carbon-web-components/examples/codesandbox/form/redux-form/src/index.js b/web-components/packages/carbon-web-components/examples/codesandbox/form/redux-form/src/index.js index c1cdecaa2410..13060a06c35b 100644 --- a/web-components/packages/carbon-web-components/examples/codesandbox/form/redux-form/src/index.js +++ b/web-components/packages/carbon-web-components/examples/codesandbox/form/redux-form/src/index.js @@ -15,7 +15,7 @@ import { Field, SubmissionError, reduxForm, reducer as reduxFormReducer } from ' import BXBtn from '@carbon/web-components/es/components-react/button/button.js'; import BXFormItem from '@carbon/web-components/es/components-react/form/form-item.js'; import CDSTextInput from '@carbon/web-components/es/components-react/text-input/text-input.js'; -import BXInlineNotification from '@carbon/web-components/es/components-react/notification/inline-notification.js'; +import CDSInlineNotification from '@carbon/web-components/es/components-react/notification/inline-notification.js'; import './index.css'; const reducer = combineReducers({ @@ -70,7 +70,7 @@ const SubmitValidationForm = reduxForm({ })(({ error, handleSubmit, pristine, reset, submitting }) => (
{error && ( - + )} diff --git a/web-components/packages/carbon-web-components/src/components/notification/actionable-notification-button.ts b/web-components/packages/carbon-web-components/src/components/notification/actionable-notification-button.ts new file mode 100644 index 000000000000..c099a1ba4fb2 --- /dev/null +++ b/web-components/packages/carbon-web-components/src/components/notification/actionable-notification-button.ts @@ -0,0 +1,34 @@ +/** + * @license + * + * Copyright IBM Corp. 2019, 2023 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { customElement } from 'lit/decorators.js'; +import { prefix } from '../../globals/settings'; +import CDSButton from '../button/button'; +import styles from './actionable-notification.scss'; + +/** + * Actionable notification action button. + * + * @element cds-actionable-notification-button + */ +@customElement(`${prefix}-actionable-notification-button`) +class CDSActionableNotificationButton extends CDSButton { + update(changedProperties) { + super.update(changedProperties); + this.shadowRoot!.getElementById('button')?.classList.add( + `${prefix}--actionable-notification__action-button` + ); + + this.setAttribute('size', 'sm'); + } + + static styles = styles; +} + +export default CDSActionableNotificationButton; diff --git a/web-components/packages/carbon-web-components/src/components/notification/actionable-notification-story.ts b/web-components/packages/carbon-web-components/src/components/notification/actionable-notification-story.ts new file mode 100644 index 000000000000..57f51defee3f --- /dev/null +++ b/web-components/packages/carbon-web-components/src/components/notification/actionable-notification-story.ts @@ -0,0 +1,120 @@ +/** + * @license + * + * Copyright IBM Corp. 2019, 2023 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { html } from 'lit'; +import { ifDefined } from 'lit/directives/if-defined.js'; +import { action } from '@storybook/addon-actions'; +import { boolean, select } from '@storybook/addon-knobs'; +import textNullable from '../../../.storybook/knob-text-nullable'; +import { NOTIFICATION_KIND } from './inline-notification'; +import './actionable-notification'; +import './actionable-notification-button'; +import storyDocs from './notification-story.mdx'; +import { prefix } from '../../globals/settings'; +import kinds from './stories/helper'; +import '../button/button'; + +const noop = () => {}; + +export const Default = () => { + return html` + + Action + + `; +}; + +export const Playground = (args) => { + const { + actionButtonLabel, + closeOnEscape, + hasFocus, + kind, + title, + subtitle, + hideCloseButton, + lowContrast, + role, + inline, + statusIconDescription, + disableClose, + onBeforeClose = noop, + onClose = noop, + } = args?.[`${prefix}-actionable-notification`] ?? {}; + const handleBeforeClose = (event: CustomEvent) => { + onBeforeClose(event); + if (disableClose) { + event.preventDefault(); + } + }; + return html` + + ${actionButtonLabel} + + `; +}; + +Playground.parameters = { + knobs: { + [`${prefix}-actionable-notification`]: () => ({ + actionButtonLabel: textNullable( + 'Action button label (action-button-label)', + 'Action' + ), + closeOnEscape: boolean('Close on escape (close-on-escape)', true), + hasFocus: boolean('Has focus (has-focus)', false), + hideCloseButton: boolean( + 'Hide the close button (hide-close-button)', + false + ), + inline: boolean('Inline (inline)', false), + kind: select( + 'The notification kind (kind)', + kinds, + NOTIFICATION_KIND.ERROR + ), + lowContrast: boolean('Use low contrast variant (low-contrast)', false), + role: textNullable('Role (role)', 'alertdialog'), + subtitle: textNullable('Subtitle (subtitle)', 'Subtitle text goes here'), + statusIconDescription: textNullable( + 'statusIconDescription (status-icon-description)', + 'notification' + ), + title: textNullable('Title (title)', 'Notification title'), + onBeforeClose: action(`${prefix}-notification-beingclosed`), + onClose: action(`${prefix}-notification-closed`), + }), + }, +}; + +export default { + title: 'Components/Notifications/Actionable', + parameters: { + ...storyDocs.parameters, + }, +}; diff --git a/web-components/packages/carbon-web-components/src/components/notification/actionable-notification.scss b/web-components/packages/carbon-web-components/src/components/notification/actionable-notification.scss new file mode 100644 index 000000000000..80d33946daf2 --- /dev/null +++ b/web-components/packages/carbon-web-components/src/components/notification/actionable-notification.scss @@ -0,0 +1,170 @@ +// +// Copyright IBM Corp. 2019, 2023 +// +// This source code is licensed under the Apache-2.0 license found in the +// LICENSE file in the root directory of this source tree. +// + +$css--plex: true !default; + +@use '@carbon/styles/scss/config' as *; +@use '@carbon/styles/scss/colors' as *; +@use '@carbon/styles/scss/theme' as *; +@use '@carbon/styles/scss/spacing' as *; +@use '@carbon/styles/scss/components/notification/index' as *; +@use '@carbon/styles/scss/components/button'; +@use '@carbon/styles/scss/components/button/mixins' as button-mixins; +@use '@carbon/styles/scss/components/button/vars' as button-vars; + +@include actionable-notification; + +:host(#{$prefix}-actionable-notification) { + @extend .#{$prefix}--actionable-notification; + + display: none; + outline: none; +} + +:host(#{$prefix}-actionable-notification-button):not( + [low-contrast] + )[kind='tertiary'] + button { + @include button-mixins.button-theme( + transparent, + $notification-action-tertiary-inverse, + $notification-action-tertiary-inverse, + $notification-action-tertiary-inverse-hover, + currentColor, + $notification-action-tertiary-inverse-active + ); + + &:focus { + border-color: $focus-inverse; + background-color: $notification-action-tertiary-inverse; + box-shadow: inset 0 0 0 button-vars.$button-outline-width $focus-inverse, + inset 0 0 0 button-vars.$button-border-width $background-inverse; + color: $notification-action-tertiary-inverse-text; + } + + &:hover { + color: $notification-action-tertiary-inverse-text; + } + + &:active { + border-color: transparent; + background-color: $notification-action-tertiary-inverse-active; + color: $notification-action-tertiary-inverse-text; + } +} + +:host(#{$prefix}-actionable-notification-button)[low-contrast][kind='ghost'] + button { + &:hover, + &:active { + background-color: $notification-action-hover; + } + + &:focus { + outline-color: $focus; + } +} + +:host(#{$prefix}-actionable-notification-button):not( + [low-contrast] + )[kind='ghost'] + button { + color: $link-inverse; +} + +:host( + #{$prefix}-actionable-notification-button + )[hide-close-button][kind='ghost'] + button { + margin-right: $spacing-03; +} + +:host(#{$prefix}-actionable-notification):not([inline]) { + @extend .#{$prefix}--actionable-notification--toast; +} + +:host(#{$prefix}-actionable-notification[open]) { + display: flex; +} + +:host(#{$prefix}-actionable-notification[hide-close-button]) + .#{$prefix}--actionable-notification__close-button { + display: none; +} + +:host(#{$prefix}-actionable-notification[kind='success']) { + @extend .#{$prefix}--actionable-notification--success; + + &[low-contrast] { + @extend .#{$prefix}--actionable-notification--low-contrast, + .#{$prefix}--actionable-notification--success; + } +} + +:host(#{$prefix}-actionable-notification[kind='info']) { + @extend .#{$prefix}--actionable-notification--info; + + &[low-contrast] { + @extend .#{$prefix}--actionable-notification--low-contrast, + .#{$prefix}--actionable-notification--info; + } +} + +:host(#{$prefix}-actionable-notification[kind='info-square']) { + @extend .#{$prefix}--actionable-notification--info-square; + + &[low-contrast] { + @extend .#{$prefix}--actionable-notification--low-contrast, + .#{$prefix}--actionable-notification--info; + } +} + +:host(#{$prefix}-actionable-notification[kind='warning']) { + @extend .#{$prefix}--actionable-notification--warning; + + &[low-contrast] { + @extend .#{$prefix}--actionable-notification--low-contrast, + .#{$prefix}--actionable-notification--warning; + } + + /* TODO: Remove this once the following issue with icon fill is resolved: + ** https://github.com/carbon-design-system/carbon/issues/13616 + */ + .#{$prefix}--inline-notification__icon path[data-icon-path='inner-path'] { + fill: $black-100; + opacity: 1; + } +} + +:host(#{$prefix}-actionable-notification[kind='warning-alt']) { + @extend .#{$prefix}--actionable-notification--warning-alt; + + &[low-contrast] { + @extend .#{$prefix}--actionable-notification--low-contrast, + .#{$prefix}--actionable-notification--warning; + } + + /* TODO: Remove this once the following issue with icon fill is resolved: + ** https://github.com/carbon-design-system/carbon/issues/13616 + */ + .#{$prefix}--inline-notification__icon, + .#{$prefix}--toast-notification__icon { + path[data-icon-path='inner-path'] { + fill: $black-100; + opacity: 1; + } + } +} + +:host(#{$prefix}-actionable-notification[kind='error']) { + @extend .#{$prefix}--actionable-notification--error; + + &[low-contrast] { + @extend .#{$prefix}--actionable-notification--low-contrast, + .#{$prefix}--actionable-notification--error; + } +} diff --git a/web-components/packages/carbon-web-components/src/components/notification/actionable-notification.ts b/web-components/packages/carbon-web-components/src/components/notification/actionable-notification.ts new file mode 100644 index 000000000000..ad38f1b39001 --- /dev/null +++ b/web-components/packages/carbon-web-components/src/components/notification/actionable-notification.ts @@ -0,0 +1,184 @@ +/** + * @license + * + * Copyright IBM Corp. 2019, 2023 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ +import CheckmarkFilled20 from '@carbon/icons/lib/checkmark--filled/20'; +import ErrorFilled20 from '@carbon/icons/lib/error--filled/20'; +import InformationFilled20 from '@carbon/icons/lib/information--filled/20'; +import InformationSquareFilled20 from '@carbon/icons/lib/information--square--filled/20'; +import WarningFilled20 from '@carbon/icons/lib/warning--filled/20'; +import WarningAltFilled20 from '@carbon/icons/lib/warning--alt--filled/20'; +import { html, svg } from 'lit'; +import { property, customElement } from 'lit/decorators.js'; +import { prefix } from '../../globals/settings'; +import { NOTIFICATION_TYPE, NOTIFICATION_KIND } from './defs'; +import CDSInlineNotification from './inline-notification'; +import styles from './actionable-notification.scss'; +import HostListener from '../../globals/decorators/host-listener'; +import HostListenerMixin from '../../globals/mixins/host-listener'; + +/** + * The default icons, keyed by notification kind. + */ +const iconsForKinds = { + [NOTIFICATION_KIND.SUCCESS]: CheckmarkFilled20, + [NOTIFICATION_KIND.INFO]: InformationFilled20, + [NOTIFICATION_KIND.INFO_SQUARE]: InformationSquareFilled20, + [NOTIFICATION_KIND.WARNING]: WarningFilled20, + [NOTIFICATION_KIND.WARNING_ALT]: WarningAltFilled20, + [NOTIFICATION_KIND.ERROR]: ErrorFilled20, +}; + +/** + * Actionable notification. + * + * @element cds-actionable-notification + * @slot subtitle - The subtitle. + * @slot title - The title. + * @fires cds-notification-beingclosed + * The custom event fired before this notification is being closed upon a user gesture. + * Cancellation of this event stops the user-initiated action of closing this notification. + * @fires cds-notification-closed - The custom event fired after this notification is closed upon a user gesture. + */ +@customElement(`${prefix}-actionable-notification`) +class CDSActionableNotification extends HostListenerMixin( + CDSInlineNotification +) { + protected _type = NOTIFICATION_TYPE.ACTIONABLE; + + /** + * Inline notification type. + */ + @property({ type: Boolean, reflect: true }) + inline = false; + + /** + * Pass in the action button label that will be rendered within the ActionableNotification. + */ + @property({ type: String, reflect: true, attribute: 'action-button-label' }) + actionButtonLabel = ''; + + /** + * Specify if pressing the escape key should close notifications + */ + @property({ type: Boolean, reflect: true, attribute: 'close-on-escape' }) + closeOnEscape = true; + + /** + * Specify if focus should be moved to the component when the notification contains actions + */ + @property({ type: Boolean, reflect: true, attribute: 'has-focus' }) + hasFocus = true; + + /** + * Handles `keydown` event on this event. + * Escape will close the notification if `closeOnEscape` is true + */ + @HostListener('keydown') + // @ts-ignore: The decorator refers to this method but TS thinks this method is not referred to + private _handleKeyDown = async (event: KeyboardEvent) => { + const { key } = event; + if (this.closeOnEscape && key === 'Escape') { + this.open = false; + } + }; + + connectedCallback() { + if (!this.hasAttribute('role')) { + this.setAttribute('role', 'alertdialog'); + } + super.connectedCallback(); + } + + protected _renderIcon() { + const { statusIconDescription, kind, inline } = this; + const { [kind]: icon } = iconsForKinds; + return !icon + ? undefined + : icon({ + class: `${prefix}--${inline ? 'inline' : 'toast'}-notification__icon`, + children: !statusIconDescription + ? undefined + : svg`${statusIconDescription}`, + }); + } + + protected _renderText() { + const { subtitle, title, _type: type } = this; + return html` +
+
+
+ ${title} +
+
+ ${subtitle} +
+ +
+
+ `; + } + + /** + * The caption. + */ + @property() + caption = ''; + + updated(changedProperties) { + super.updated(changedProperties); + const button = this.querySelector( + (this.constructor as typeof CDSActionableNotification) + .selectorActionButton + ); + if (changedProperties.has('inline')) { + button?.setAttribute('kind', this.inline ? 'ghost' : 'tertiary'); + } + if (changedProperties.has('lowContrast')) { + if (this.lowContrast) { + button?.setAttribute('low-contrast', 'true'); + } else { + button?.removeAttribute('low-contrast'); + } + } + if (changedProperties.has('hideCloseButton')) { + if (this.hideCloseButton) { + button?.setAttribute('hide-close-button', 'true'); + } else { + button?.removeAttribute('hide-close-button'); + } + } + if (changedProperties.has('hasFocus')) { + if (this.hasFocus) { + this.focus(); + } + } + } + + render() { + const { _type: type } = this; + return html` +
+ ${this._renderIcon()} ${this._renderText()} +
+ + ${this._renderButton()} + `; + } + + /** + * A selector that will return the action button element + */ + static get selectorActionButton() { + return `${prefix}-actionable-notification-button`; + } + + static styles = styles; // `styles` here is a `CSSResult` generated by custom WebPack loader +} + +export default CDSActionableNotification; diff --git a/web-components/packages/carbon-web-components/src/components/notification/defs.ts b/web-components/packages/carbon-web-components/src/components/notification/defs.ts index a714f6118603..99dd052da03a 100644 --- a/web-components/packages/carbon-web-components/src/components/notification/defs.ts +++ b/web-components/packages/carbon-web-components/src/components/notification/defs.ts @@ -21,11 +21,21 @@ export enum NOTIFICATION_KIND { */ INFO = 'info', + /** + * Informational square icon notification. + */ + INFO_SQUARE = 'info-square', + /** * Warning notification. */ WARNING = 'warning', + /** + * Warning Alt notification. + */ + WARNING_ALT = 'warning-alt', + /** * Error notification. */ @@ -47,4 +57,13 @@ export enum NOTIFICATION_TYPE { * They usually appear at the bottom of the screen and disappear after a few seconds. */ TOAST = 'toast', + + /** + * Actionable notifications allow for interactive elements within a notification styled like an inline + * or toast notification. Actionable notifications, since they require user interaction, take focus when + * triggered and can be highly disruptive to screen readers and keyboard users. With actionable notifications, + * only one action is allowed per notification. This action frequently takes users to a flow or page related + * to the message, where they can resolve the notification. + */ + ACTIONABLE = 'actionable', } diff --git a/web-components/packages/carbon-web-components/src/components/notification/index.ts b/web-components/packages/carbon-web-components/src/components/notification/index.ts index 04e94affb585..899b14e76237 100644 --- a/web-components/packages/carbon-web-components/src/components/notification/index.ts +++ b/web-components/packages/carbon-web-components/src/components/notification/index.ts @@ -9,3 +9,5 @@ import './inline-notification'; import './toast-notification'; +import './actionable-notification'; +import './actionable-notification-button'; diff --git a/web-components/packages/carbon-web-components/src/components/notification/inline-notification-story.ts b/web-components/packages/carbon-web-components/src/components/notification/inline-notification-story.ts new file mode 100644 index 000000000000..4113f290c220 --- /dev/null +++ b/web-components/packages/carbon-web-components/src/components/notification/inline-notification-story.ts @@ -0,0 +1,103 @@ +/** + * @license + * + * Copyright IBM Corp. 2019, 2023 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { html } from 'lit'; +import { action } from '@storybook/addon-actions'; +import { ifDefined } from 'lit/directives/if-defined.js'; +import { boolean, select } from '@storybook/addon-knobs'; +import storyDocs from './notification-story.mdx'; +import { NOTIFICATION_KIND } from './inline-notification'; +import { prefix } from '../../globals/settings'; +import textNullable from '../../../.storybook/knob-text-nullable'; +import kinds from './stories/helper'; + +const noop = () => {}; + +export const Default = () => { + return html` + + + `; +}; + +export const Playground = (args) => { + const { + kind, + title, + subtitle, + hideCloseButton, + lowContrast, + role, + statusIconDescription, + timeout, + disableClose, + onBeforeClose = noop, + onClose = noop, + } = args?.[`${prefix}-inline-notification`] ?? {}; + const handleBeforeClose = (event: CustomEvent) => { + onBeforeClose(event); + if (disableClose) { + event.preventDefault(); + } + }; + return html` + + + `; +}; + +Playground.parameters = { + knobs: { + [`${prefix}-inline-notification`]: () => ({ + hideCloseButton: boolean( + 'Hide the close button (hide-close-button)', + false + ), + kind: select( + 'The notification kind (kind)', + kinds, + NOTIFICATION_KIND.INFO + ), + lowContrast: boolean('Use low contrast variant (low-contrast)', false), + role: select( + 'Role (role)', + { alert: 'alert', log: 'log', status: 'status' }, + 'status' + ), + statusIconDescription: textNullable( + 'statusIconDescription (status-icon-description)', + 'notification' + ), + subtitle: textNullable('Subtitle (subtitle)', 'Subtitle text goes here'), + title: textNullable('Title (title)', 'Notification title'), + onBeforeClose: action(`${prefix}-notification-beingclosed`), + onClose: action(`${prefix}-notification-closed`), + }), + }, +}; + +export default { + title: 'Components/Notifications/Inline', + parameters: { + ...storyDocs.parameters, + }, +}; diff --git a/web-components/packages/carbon-web-components/src/components/notification/inline-notification.scss b/web-components/packages/carbon-web-components/src/components/notification/inline-notification.scss index e40e8ec18c6c..d128ed645fc1 100644 --- a/web-components/packages/carbon-web-components/src/components/notification/inline-notification.scss +++ b/web-components/packages/carbon-web-components/src/components/notification/inline-notification.scss @@ -8,6 +8,7 @@ $css--plex: true !default; @use '@carbon/styles/scss/config' as *; +@use '@carbon/styles/scss/colors' as *; @use '@carbon/styles/scss/utilities/focus-outline' as *; @use '@carbon/styles/scss/components/notification/index' as *; @use '@carbon/styles/scss/components/button'; @@ -53,6 +54,15 @@ $css--plex: true !default; } } +:host(#{$prefix}-inline-notification[kind='info-square']) { + @extend .#{$prefix}--inline-notification--info-square; + + &[low-contrast] { + @extend .#{$prefix}--inline-notification--low-contrast, + .#{$prefix}--inline-notification--info-square; + } +} + :host(#{$prefix}-inline-notification[kind='warning']) { @extend .#{$prefix}--inline-notification--warning; @@ -70,3 +80,19 @@ $css--plex: true !default; .#{$prefix}--inline-notification--error; } } + +:host(#{$prefix}-inline-notification[kind='warning-alt']) { + @extend .#{$prefix}--inline-notification--warning-alt; + + &[low-contrast] { + @extend .#{$prefix}--inline-notification--low-contrast, + .#{$prefix}--inline-notification--warning-alt; + } + + /* TODO: Remove this once the following issue with icon fill is resolved: + ** https://github.com/carbon-design-system/carbon/issues/13616 + */ + .#{$prefix}--inline-notification__icon path[data-icon-path='inner-path'] { + fill: $black-100; + } +} diff --git a/web-components/packages/carbon-web-components/src/components/notification/inline-notification.ts b/web-components/packages/carbon-web-components/src/components/notification/inline-notification.ts index 07898bf374e9..ed83b6a23c79 100644 --- a/web-components/packages/carbon-web-components/src/components/notification/inline-notification.ts +++ b/web-components/packages/carbon-web-components/src/components/notification/inline-notification.ts @@ -8,9 +8,12 @@ */ import CheckmarkFilled20 from '@carbon/icons/lib/checkmark--filled/20'; -import Close20 from '@carbon/icons/lib/close/20'; +import Close16 from '@carbon/icons/lib/close/16'; import ErrorFilled20 from '@carbon/icons/lib/error--filled/20'; +import InformationFilled20 from '@carbon/icons/lib/information--filled/20'; +import InformationSquareFilled20 from '@carbon/icons/lib/information--square--filled/20'; import WarningFilled20 from '@carbon/icons/lib/warning--filled/20'; +import WarningAltFilled20 from '@carbon/icons/lib/warning--alt--filled/20'; import { LitElement, html, svg } from 'lit'; import { property, customElement } from 'lit/decorators.js'; import { ifDefined } from 'lit/directives/if-defined.js'; @@ -26,8 +29,10 @@ export { NOTIFICATION_KIND, NOTIFICATION_TYPE }; */ const iconsForKinds = { [NOTIFICATION_KIND.SUCCESS]: CheckmarkFilled20, - [NOTIFICATION_KIND.INFO]: undefined, + [NOTIFICATION_KIND.INFO]: InformationFilled20, + [NOTIFICATION_KIND.INFO_SQUARE]: InformationSquareFilled20, [NOTIFICATION_KIND.WARNING]: WarningFilled20, + [NOTIFICATION_KIND.WARNING_ALT]: WarningAltFilled20, [NOTIFICATION_KIND.ERROR]: ErrorFilled20, }; @@ -43,7 +48,7 @@ const iconsForKinds = { * @fires cds-notification-closed - The custom event fired after this notification is closed upon a user gesture. */ @customElement(`${prefix}-inline-notification`) -class BXInlineNotification extends FocusMixin(LitElement) { +class CDSInlineNotification extends FocusMixin(LitElement) { /** * Current timeout identifier */ @@ -106,7 +111,7 @@ class BXInlineNotification extends FocusMixin(LitElement) { if ( this.dispatchEvent( new CustomEvent( - (this.constructor as typeof BXInlineNotification).eventBeforeClose, + (this.constructor as typeof CDSInlineNotification).eventBeforeClose, init ) ) @@ -114,7 +119,7 @@ class BXInlineNotification extends FocusMixin(LitElement) { this.open = false; this.dispatchEvent( new CustomEvent( - (this.constructor as typeof BXInlineNotification).eventClose, + (this.constructor as typeof CDSInlineNotification).eventClose, init ) ); @@ -127,7 +132,7 @@ class BXInlineNotification extends FocusMixin(LitElement) { */ protected _renderButton() { const { - closeButtonLabel, + ariaLabel, _type: type, _handleClickCloseButton: handleClickCloseButton, } = this; @@ -135,10 +140,10 @@ class BXInlineNotification extends FocusMixin(LitElement) { @@ -167,21 +172,23 @@ class BXInlineNotification extends FocusMixin(LitElement) { * @returns The template part for the icon. */ protected _renderIcon() { - const { iconLabel, kind, _type: type } = this; + const { statusIconDescription, kind, _type: type } = this; const { [kind]: icon } = iconsForKinds; return !icon ? undefined : icon({ class: `${prefix}--${type}-notification__icon`, - children: !iconLabel ? undefined : svg`${iconLabel}`, + children: !statusIconDescription + ? undefined + : svg`${statusIconDescription}`, }); } /** - * The a11y text for the close button. + * Provide a description for "close" icon button that can be read by screen readers */ - @property({ attribute: 'close-button-label' }) - closeButtonLabel!: string; + @property({ attribute: 'aria-label' }) + ariaLabel!: string; /** * `true` to hide the close button. @@ -190,10 +197,10 @@ class BXInlineNotification extends FocusMixin(LitElement) { hideCloseButton = false; /** - * The a11y text for the icon. + * Provide a description for "status" icon that can be read by screen readers */ - @property({ attribute: 'icon-label' }) - iconLabel!: string; + @property({ attribute: 'status-icon-description' }) + statusIconDescription!: string; /** * Notification kind. @@ -214,7 +221,7 @@ class BXInlineNotification extends FocusMixin(LitElement) { open = true; /** - * Notification time in ms until gets closed. + * Specify an optional duration the notification should be closed in */ @property({ type: Number, reflect: true }) timeout: number | null = null; @@ -279,4 +286,4 @@ class BXInlineNotification extends FocusMixin(LitElement) { static styles = styles; // `styles` here is a `CSSResult` generated by custom WebPack loader } -export default BXInlineNotification; +export default CDSInlineNotification; diff --git a/web-components/packages/carbon-web-components/src/components/notification/notification-story.mdx b/web-components/packages/carbon-web-components/src/components/notification/notification-story.mdx index f6eba4909b60..ac53b63f09e8 100644 --- a/web-components/packages/carbon-web-components/src/components/notification/notification-story.mdx +++ b/web-components/packages/carbon-web-components/src/components/notification/notification-story.mdx @@ -34,6 +34,27 @@ import '@carbon/web-components/es/components/notification/index.js'; ``` +## Actionable notification + +Actionable notifications allow for interactive elements within a notification +styled like an inline or toast notification. Actionable notifications, since +they require user interaction, take focus when triggered and can be highly +disruptive to screen readers and keyboard users. With actionable notifications, +only one action is allowed per notification. This action frequently takes users +to a flow or page related to the message, where they can resolve the +notification. + +```html + + Action + +``` + ## Inline notification Inline notifications show up in task flows, to notify users of the status of an @@ -79,6 +100,14 @@ example: } ``` +## `` attributes, properties and events + +Note: For `boolean` attributes, `true` means simply setting the attribute (e.g. +``) and `false` means not setting the +attribute (e.g. `` without `open` attribute). + + + ## `` attributes, properties and events Note: For `boolean` attributes, `true` means simply setting the attribute (e.g. diff --git a/web-components/packages/carbon-web-components/src/components/notification/stories/helper.ts b/web-components/packages/carbon-web-components/src/components/notification/stories/helper.ts new file mode 100644 index 000000000000..a6813a882aa9 --- /dev/null +++ b/web-components/packages/carbon-web-components/src/components/notification/stories/helper.ts @@ -0,0 +1,22 @@ +/** + * @license + * + * Copyright IBM Corp. 2023 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { NOTIFICATION_KIND } from '../inline-notification'; + +const kinds = { + [`Error (${NOTIFICATION_KIND.ERROR})`]: NOTIFICATION_KIND.ERROR, + [`Info (${NOTIFICATION_KIND.INFO})`]: NOTIFICATION_KIND.INFO, + [`Info (${NOTIFICATION_KIND.INFO_SQUARE})`]: NOTIFICATION_KIND.INFO_SQUARE, + [`Success (${NOTIFICATION_KIND.SUCCESS})`]: NOTIFICATION_KIND.SUCCESS, + [`Warning (${NOTIFICATION_KIND.WARNING})`]: NOTIFICATION_KIND.WARNING, + [`Warning Alt (${NOTIFICATION_KIND.WARNING_ALT})`]: + NOTIFICATION_KIND.WARNING_ALT, +}; + +export default kinds; diff --git a/web-components/packages/carbon-web-components/src/components/notification/notification-story.ts b/web-components/packages/carbon-web-components/src/components/notification/toast-notification-story.ts similarity index 50% rename from web-components/packages/carbon-web-components/src/components/notification/notification-story.ts rename to web-components/packages/carbon-web-components/src/components/notification/toast-notification-story.ts index dd5c0fb8d275..a09a1011f969 100644 --- a/web-components/packages/carbon-web-components/src/components/notification/notification-story.ts +++ b/web-components/packages/carbon-web-components/src/components/notification/toast-notification-story.ts @@ -16,98 +16,34 @@ import { NOTIFICATION_KIND } from './inline-notification'; import './toast-notification'; import storyDocs from './notification-story.mdx'; import { prefix } from '../../globals/settings'; - -const kinds = { - [`Success (${NOTIFICATION_KIND.SUCCESS})`]: NOTIFICATION_KIND.SUCCESS, - [`Info (${NOTIFICATION_KIND.INFO})`]: NOTIFICATION_KIND.INFO, - [`Warning (${NOTIFICATION_KIND.WARNING})`]: NOTIFICATION_KIND.WARNING, - [`Error (${NOTIFICATION_KIND.ERROR})`]: NOTIFICATION_KIND.ERROR, -}; +import kinds from './stories/helper'; const noop = () => {}; -export const inline = (args) => { - const { - kind, - title, - subtitle, - hideCloseButton, - lowContrast, - closeButtonLabel, - iconLabel, - open, - timeout, - disableClose, - onBeforeClose = noop, - onClose = noop, - } = args?.[`${prefix}-inline-notification`] ?? {}; - const handleBeforeClose = (event: CustomEvent) => { - onBeforeClose(event); - if (disableClose) { - event.preventDefault(); - } - }; +export const Default = () => { return html` - - + + `; }; -inline.parameters = { - knobs: { - [`${prefix}-inline-notification`]: () => ({ - kind: select( - 'The notification kind (kind)', - kinds, - NOTIFICATION_KIND.INFO - ), - title: textNullable('Title (title)', 'Notification title'), - subtitle: textNullable('Subtitle (subtitle)', 'Subtitle text goes here.'), - hideCloseButton: boolean( - 'Hide the close button (hide-close-button)', - false - ), - lowContrast: boolean('Use low contrast variant (low-contrast)', false), - closeButtonLabel: textNullable( - 'a11y label for the close button (close-button-label)', - '' - ), - iconLabel: textNullable('a11y label for the icon (icon-label)', ''), - open: boolean('Open (open)', true), - timeout: textNullable('Timeout (in ms)', ''), - disableClose: boolean( - `Disable user-initiated close action (Call event.preventDefault() in ${prefix}-notification-beingclosed event)`, - false - ), - onBeforeClose: action(`${prefix}-notification-beingclosed`), - onClose: action(`${prefix}-notification-closed`), - }), - }, -}; - -export const toast = (args) => { +export const Playground = (args) => { const { kind, title, subtitle, caption, hideCloseButton, + statusIconDescription, lowContrast, - closeButtonLabel, - iconLabel, - open, timeout, + role, disableClose, onBeforeClose = noop, onClose = noop, @@ -124,11 +60,10 @@ export const toast = (args) => { title="${ifDefined(title)}" subtitle="${ifDefined(subtitle)}" caption="${ifDefined(caption)}" + role="${ifDefined(role)}" ?hide-close-button="${hideCloseButton}" ?low-contrast="${lowContrast}" - close-button-label="${ifDefined(closeButtonLabel)}" - icon-label="${ifDefined(iconLabel)}" - ?open="${open}" + status-icon-description="${ifDefined(statusIconDescription)}" timeout="${ifDefined(timeout)}" @cds-notification-beingclosed="${handleBeforeClose}" @cds-notification-closed="${onClose}"> @@ -136,17 +71,40 @@ export const toast = (args) => { `; }; -toast.parameters = { +Playground.parameters = { knobs: { [`${prefix}-toast-notification`]: () => ({ - ...inline.parameters.knobs[`${prefix}-inline-notification`](), - caption: textNullable('Caption (caption)', 'Time stamp [00:00:00]'), + caption: textNullable('Caption (caption)', '00:00:00 AM'), + hideCloseButton: boolean( + 'Hide the close button (hide-close-button)', + false + ), + kind: select( + 'The notification kind (kind)', + kinds, + NOTIFICATION_KIND.INFO + ), + lowContrast: boolean('Use low contrast variant (low-contrast)', false), + role: select( + 'Role (role)', + { alert: 'alert', log: 'log', status: 'status' }, + 'status' + ), + statusIconDescription: textNullable( + 'statusIconDescription (status-icon-description)', + 'notification' + ), + subtitle: textNullable('Subtitle (subtitle)', 'Subtitle text goes here'), + timeout: textNullable('Timeout in ms (timeout)', '0'), + title: textNullable('Title (title)', 'Notification title'), + onBeforeClose: action(`${prefix}-notification-beingclosed`), + onClose: action(`${prefix}-notification-closed`), }), }, }; export default { - title: 'Components/Notifications', + title: 'Components/Notifications/Toast', parameters: { ...storyDocs.parameters, }, diff --git a/web-components/packages/carbon-web-components/src/components/notification/toast-notification.scss b/web-components/packages/carbon-web-components/src/components/notification/toast-notification.scss index 73df9ec3c9ad..a5465ba2ff7f 100644 --- a/web-components/packages/carbon-web-components/src/components/notification/toast-notification.scss +++ b/web-components/packages/carbon-web-components/src/components/notification/toast-notification.scss @@ -8,6 +8,7 @@ $css--plex: true !default; @use '@carbon/styles/scss/config' as *; +@use '@carbon/styles/scss/colors' as *; @use '@carbon/styles/scss/components/notification/index' as *; @use '@carbon/styles/scss/components/button'; @@ -48,6 +49,15 @@ $css--plex: true !default; } } +:host(#{$prefix}-toast-notification[kind='info-square']) { + @extend .#{$prefix}--toast-notification--info-square; + + &[low-contrast] { + @extend .#{$prefix}--toast-notification--low-contrast, + .#{$prefix}--toast-notification--info; + } +} + :host(#{$prefix}-toast-notification[kind='warning']) { @extend .#{$prefix}--toast-notification--warning; @@ -57,6 +67,22 @@ $css--plex: true !default; } } +:host(#{$prefix}-toast-notification[kind='warning-alt']) { + @extend .#{$prefix}--toast-notification--warning-alt; + + &[low-contrast] { + @extend .#{$prefix}--toast-notification--low-contrast, + .#{$prefix}--toast-notification--warning; + } + + /* TODO: Remove this once the following issue with icon fill is resolved: + ** https://github.com/carbon-design-system/carbon/issues/13616 + */ + .#{$prefix}--toast-notification__icon path[data-icon-path='inner-path'] { + fill: $black-100; + } +} + :host(#{$prefix}-toast-notification[kind='error']) { @extend .#{$prefix}--toast-notification--error; diff --git a/web-components/packages/carbon-web-components/src/components/notification/toast-notification.ts b/web-components/packages/carbon-web-components/src/components/notification/toast-notification.ts index 0391e89c3818..c621c9c44b8a 100644 --- a/web-components/packages/carbon-web-components/src/components/notification/toast-notification.ts +++ b/web-components/packages/carbon-web-components/src/components/notification/toast-notification.ts @@ -11,7 +11,7 @@ import { html } from 'lit'; import { property, customElement } from 'lit/decorators.js'; import { prefix } from '../../globals/settings'; import { NOTIFICATION_TYPE } from './defs'; -import BXInlineNotification from './inline-notification'; +import CDSInlineNotification from './inline-notification'; import styles from './toast-notification.scss'; /** @@ -26,7 +26,7 @@ import styles from './toast-notification.scss'; * @fires cds-notification-closed - The custom event fired after this notification is closed upon a user gesture. */ @customElement(`${prefix}-toast-notification`) -class BXToastNotification extends BXInlineNotification { +class CDSToastNotification extends CDSInlineNotification { protected _type = NOTIFICATION_TYPE.TOAST; protected _renderText() { @@ -48,7 +48,7 @@ class BXToastNotification extends BXInlineNotification { } /** - * The caption. + * Specify the caption */ @property() caption = ''; @@ -67,4 +67,4 @@ class BXToastNotification extends BXInlineNotification { static styles = styles; // `styles` here is a `CSSResult` generated by custom WebPack loader } -export default BXToastNotification; +export default CDSToastNotification; diff --git a/web-components/packages/carbon-web-components/src/index.ts b/web-components/packages/carbon-web-components/src/index.ts index 7ef00b138f74..d168de8e96ff 100644 --- a/web-components/packages/carbon-web-components/src/index.ts +++ b/web-components/packages/carbon-web-components/src/index.ts @@ -57,6 +57,8 @@ export { default as CDSModalHeading } from './components/modal/modal-heading'; export { default as CDSModalLabel } from './components/modal/modal-label'; export { default as CDSMultiSelect } from './components/multi-select/multi-select'; export { default as CDSMultiSelectItem } from './components/multi-select/multi-select-item'; +export { default as CDSActionableNotification } from './components/notification/actionable-notification'; +export { default as CDSActionableNotificationButton } from './components/notification/actionable-notification-button'; export { default as CDSInlineNotification } from './components/notification/inline-notification'; export { default as CDSToastNotification } from './components/notification/toast-notification'; export { default as CDSNumberInput } from './components/number-input/number-input'; diff --git a/web-components/packages/carbon-web-components/src/typings/jsx-elements.d.ts b/web-components/packages/carbon-web-components/src/typings/jsx-elements.d.ts index ca1dff2c1602..054d0527490a 100644 --- a/web-components/packages/carbon-web-components/src/typings/jsx-elements.d.ts +++ b/web-components/packages/carbon-web-components/src/typings/jsx-elements.d.ts @@ -59,6 +59,8 @@ declare global { 'cds-modal-label': any; 'cds-multi-select': any; 'cds-multi-select-item': any; + 'cds-actionable-notification': any; + 'cds-actionable-notification-button': any; 'cds-inline-notification': any; 'cds-toast-notification': any; 'cds-number-input': any; diff --git a/web-components/packages/carbon-web-components/tests/spec/notification_spec.ts b/web-components/packages/carbon-web-components/tests/spec/notification_spec.ts index bba408955234..90a0b4a1bb87 100644 --- a/web-components/packages/carbon-web-components/tests/spec/notification_spec.ts +++ b/web-components/packages/carbon-web-components/tests/spec/notification_spec.ts @@ -8,13 +8,13 @@ */ import { render } from 'lit'; -import BXInlineNotification, { +import CDSInlineNotification, { NOTIFICATION_KIND, } from '../../src/components/notification/inline-notification'; -import { inline } from '../../src/components/notification/notification-story'; +import { Playground } from '../../src/components/notification/inline-notification-story'; const inlineTemplate = (props?) => - inline({ + Playground({ 'cds-inline-notification': props, }); @@ -49,7 +49,7 @@ describe('cds-inline-notification', function () { }); describe('Closing', function () { - let notification: BXInlineNotification | null; + let notification: CDSInlineNotification | null; beforeEach(async function () { render(inlineTemplate(), document.body); @@ -71,10 +71,10 @@ describe('cds-inline-notification', function () { let notification; beforeEach(async function () { - const initializeTimerCloseEvent = (BXInlineNotification.prototype as any) + const initializeTimerCloseEvent = (CDSInlineNotification.prototype as any) ._handleUserOrTimerInitiatedClose; spyOn( - BXInlineNotification.prototype as any, + CDSInlineNotification.prototype as any, '_initializeTimeout' ).and.callFake(function () { // TODO: See if we can get around TS2683 diff --git a/web-components/packages/carbon-web-components/tests/spec/slider_spec.ts b/web-components/packages/carbon-web-components/tests/spec/slider_spec.ts index 8a3d72127a75..69968c8c295e 100644 --- a/web-components/packages/carbon-web-components/tests/spec/slider_spec.ts +++ b/web-components/packages/carbon-web-components/tests/spec/slider_spec.ts @@ -8,7 +8,7 @@ */ import { html, render } from 'lit'; -import { Default } from '../../src/components/slider/slider-story'; +import { Playground } from '../../src/components/slider/slider-story'; /** * @param formData A `FormData` instance. @@ -24,7 +24,7 @@ const getValues = (formData: FormData) => { }; const template = (props?) => - Default({ + Playground({ 'cds-slider': props, });