From cff494c84eaf3100861560741d513b80217b0772 Mon Sep 17 00:00:00 2001 From: Alessandra Davila Date: Thu, 16 Jan 2020 15:38:02 -0600 Subject: [PATCH] refactor(notification): refactor component to use hooks (#5072) --- .../Notification/Notification-test.js | 90 +- .../components/Notification/Notification.js | 935 +++++++++--------- 2 files changed, 483 insertions(+), 542 deletions(-) diff --git a/packages/react/src/components/Notification/Notification-test.js b/packages/react/src/components/Notification/Notification-test.js index 7d3154bf3702..cff010fbb3dc 100644 --- a/packages/react/src/components/Notification/Notification-test.js +++ b/packages/react/src/components/Notification/Notification-test.js @@ -149,7 +149,7 @@ describe('ToastNotification', () => { }); describe('events and state', () => { it('initial open state set to true', () => { - const mountedToast = mount( + const wrapper = mount( { /> ); - expect(mountedToast.state().open).toBe(true); + expect(wrapper.children().length > 0).toBe(true); }); it('sets open state to false when close button is clicked', () => { - const mountedToast = mount( + const wrapper = mount( { /> ); - mountedToast.find('button').simulate('click'); - expect(mountedToast.state().open).toEqual(false); + wrapper.find('button').simulate('click'); + expect(wrapper.children().length).toBe(0); }); it('renders null when open state is false', () => { - const mountedToast = mount( + const wrapper = mount( { /> ); - mountedToast.setState({ open: false }); - expect(mountedToast.html()).toBeNull(); + wrapper.find('button').simulate('click'); + expect(wrapper.html()).toBeNull(); }); }); }); @@ -261,7 +261,7 @@ describe('InlineNotification', () => { describe('events and state', () => { it('initial open state set to true', () => { - const mountedInline = mount( + const wrapper = mount( { /> ); - expect(mountedInline.state().open).toBe(true); + expect(wrapper.children().length > 0).toBe(true); }); it('sets open state to false when close button is clicked', () => { - const mountedInline = mount(); + const wrapper = mount( + + ); - mountedInline.find('button').simulate('click'); - expect(mountedInline.state().open).toEqual(false); + wrapper.find('button').simulate('click'); + expect(wrapper.children().length).toBe(0); }); it('renders null when open state is false', () => { - const mountedInline = mount( + const wrapper = mount( { /> ); - mountedInline.setState({ open: false }); - expect(mountedInline.html()).toBeNull(); + wrapper.find('button').simulate('click'); + expect(wrapper.html()).toBeNull(); }); }); }); // Deprecated - -const props = { - kind: 'success', - title: 'title', - subtitle: 'subtitle', - iconDescription: 'description', -}; - -describe('events and state', () => { - it('initial open state set to true', () => { - const mountedToast = mount( - - ); - const mountedInline = mount(); - - expect(mountedToast.state().open).toBe(true); - expect(mountedInline.state().open).toBe(true); - }); - - it('sets open state to false when close button is clicked', () => { - const mountedToast = mount( - - ); - const mountedInline = mount(); - - mountedToast.find('button').simulate('click'); - mountedInline.find('button').simulate('click'); - expect(mountedToast.state().open).toEqual(false); - expect(mountedInline.state().open).toEqual(false); - }); - - it('close button is not shown if hideCloseButton prop set', () => { - const mountedToast = mount( - - ); - - expect(mountedToast.find('button')).toHaveLength(0); - }); - - it('renders null when open state is false', () => { - const mountedToast = mount( - - ); - const mountedInline = mount(); - - mountedToast.setState({ open: false }); - mountedInline.setState({ open: false }); - expect(mountedToast.html()).toBeNull(); - expect(mountedInline.html()).toBeNull(); - }); -}); diff --git a/packages/react/src/components/Notification/Notification.js b/packages/react/src/components/Notification/Notification.js index 7fc482b31643..ca95cf84ed37 100644 --- a/packages/react/src/components/Notification/Notification.js +++ b/packages/react/src/components/Notification/Notification.js @@ -6,8 +6,8 @@ */ import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import classNames from 'classnames'; +import React, { useState, useEffect } from 'react'; +import cx from 'classnames'; import { settings } from 'carbon-components'; import { Close20, @@ -20,354 +20,407 @@ import Button from '../Button'; const { prefix } = settings; -export class NotificationActionButton extends Component { - static propTypes = { - /** - * Specify an optional className to be applied to the notification action button - */ - className: PropTypes.string, - - /** - * Specify the content of the notification action button. - */ - children: PropTypes.node, - - /** - * Optionally specify a click handler for the notification action button. - */ - onClick: PropTypes.func, - }; - - render() { - const { children, className, onClick, ...other } = this.props; - - const actionButtonClasses = classNames( - className, - `${prefix}--inline-notification__action-button` - ); +export function NotificationActionButton({ + children, + className: customClassName, + onClick, + ...rest +}) { + const className = cx( + customClassName, + `${prefix}--inline-notification__action-button` + ); - return ( - - ); - } + return ( + + ); } -export class NotificationButton extends Component { - static propTypes = { - /** - * Specify an optional className to be applied to the notification button - */ - className: PropTypes.string, - - /** - * Specify a label to be read by screen readers on the notification button - */ - ariaLabel: PropTypes.string, - - /** - * Optional prop to specify the type of the Button - */ - type: PropTypes.string, - - /** - * Provide a description for "close" icon that can be read by screen readers - */ - iconDescription: PropTypes.string, - - /** - * Optional prop to allow overriding the icon rendering. - * Can be a React component class - */ - renderIcon: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), - - /** - * Specify an optional icon for the Button through a string, - * if something but regular "close" icon is desirable - */ - name: PropTypes.string, - - /** - * Specify the notification type - */ - notificationType: PropTypes.oneOf(['toast', 'inline']), - }; - - static defaultProps = { - ariaLabel: 'close notification', // TODO: deprecate this prop - notificationType: 'toast', - type: 'button', - iconDescription: 'close icon', - renderIcon: Close20, - }; - - render() { - const { - ariaLabel, - className, - iconDescription, - type, - renderIcon: IconTag, - name, - notificationType, - ...other - } = this.props; - - const buttonClasses = classNames( - { - [`${prefix}--toast-notification__close-button`]: - notificationType === 'toast', - [`${prefix}--inline-notification__close-button`]: - notificationType === 'inline', - }, - className - ); +NotificationActionButton.propTypes = { + /** + * Specify an optional className to be applied to the notification action button + */ + className: PropTypes.string, + + /** + * Specify the content of the notification action button. + */ + children: PropTypes.node, + + /** + * Optionally specify a click handler for the notification action button. + */ + onClick: PropTypes.func, +}; - const iconClasses = classNames({ - [`${prefix}--toast-notification__close-icon`]: - notificationType === 'toast', - [`${prefix}--inline-notification__close-icon`]: - notificationType === 'inline', - }); - - const NotificationButtonIcon = (() => { - if (Object(IconTag) === IconTag) { - return ( - - ); - } - return null; - })(); +export function NotificationButton({ + ariaLabel, + className, + iconDescription, + type, + renderIcon: IconTag, + name, + notificationType, + ...rest +}) { + const buttonClasName = cx(className, { + [`${prefix}--toast-notification__close-button`]: + notificationType === 'toast', + [`${prefix}--inline-notification__close-button`]: + notificationType === 'inline', + }); + const iconClassName = cx({ + [`${prefix}--toast-notification__close-icon`]: notificationType === 'toast', + [`${prefix}--inline-notification__close-icon`]: + notificationType === 'inline', + }); + + return ( + + ); +} + +NotificationButton.propTypes = { + /** + * Specify an optional className to be applied to the notification button + */ + className: PropTypes.string, + + /** + * Specify a label to be read by screen readers on the notification button + */ + ariaLabel: PropTypes.string, + + /** + * Optional prop to specify the type of the Button + */ + type: PropTypes.string, + + /** + * Provide a description for "close" icon that can be read by screen readers + */ + iconDescription: PropTypes.string, + + /** + * Optional prop to allow overriding the icon rendering. + * Can be a React component class + */ + renderIcon: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), + + /** + * Specify an optional icon for the Button through a string, + * if something but regular "close" icon is desirable + */ + name: PropTypes.string, + + /** + * Specify the notification type + */ + notificationType: PropTypes.oneOf(['toast', 'inline']), +}; + +NotificationButton.defaultProps = { + ariaLabel: 'close notification', // TODO: deprecate this prop + notificationType: 'toast', + type: 'button', + iconDescription: 'close icon', + renderIcon: Close20, +}; +export function NotificationTextDetails({ + title, + subtitle, + caption, + notificationType, + children, + ...rest +}) { + if (notificationType === 'toast') { return ( - +
+

{title}

+
+ {subtitle} +
+
+ {caption} +
+ {children} +
); } -} -export class NotificationTextDetails extends Component { - static propTypes = { - /** - * Pass in the children that will be rendered in NotificationTextDetails - */ - children: PropTypes.node, - /** - * Specify the title - */ - title: PropTypes.string, - /** - * Specify the sub-title - */ - subtitle: PropTypes.node, - /** - * Specify the caption - */ - caption: PropTypes.node, - /** - * Specify the notification type - */ - notificationType: PropTypes.oneOf(['toast', 'inline']), - }; - - static defaultProps = { - title: 'title', - caption: 'caption', - notificationType: 'toast', - }; - - render() { - const { title, subtitle, caption, notificationType, ...other } = this.props; - if (notificationType === 'toast') { - return ( -
-

{title}

-
- {subtitle} -
-
- {caption} -
- {this.props.children} -
- ); - } - - if (notificationType === 'inline') { - return ( -
-

{title}

-
- {subtitle} -
- {this.props.children} + if (notificationType === 'inline') { + return ( +
+

{title}

+
+ {subtitle}
- ); - } + {children} +
+ ); } } -const useIcon = kindProp => - ({ - error: ErrorFilled20, - success: CheckmarkFilled20, - warning: WarningFilled20, - }[kindProp]); - -const NotificationIcon = ({ notificationType, kind, iconDescription }) => { - const NotificationIconX = useIcon(kind); - return !NotificationIconX ? null : ( - {iconDescription} - + ); +} + +NotificationIcon.propTypes = { + notificationType: PropTypes.oneOf(['inline', 'toast']).isRequired, + kind: PropTypes.oneOf(['error', 'success', 'warning', 'info']).isRequired, + iconDescription: PropTypes.string.isRequired, }; -export class ToastNotification extends Component { - static propTypes = { - /** - * Pass in the children that will be rendered within the ToastNotification - */ - children: PropTypes.node, - - /** - * Specify an optional className to be applied to the notification box - */ - className: PropTypes.string, - - /** - * Specify what state the notification represents - */ - kind: PropTypes.oneOf(['error', 'info', 'success', 'warning']).isRequired, - - /** - * Specify whether you are using the low contrast variant of the ToastNotification. - */ - lowContrast: PropTypes.bool, - - /** - * Specify the title - */ - title: PropTypes.string.isRequired, - - /** - * Specify the sub-title - */ - subtitle: PropTypes.node, - - /** - * By default, this value is "alert". You can also provide an alternate - * role if it makes sense from the accessibility-side - */ - role: PropTypes.string.isRequired, - - /** - * Specify the caption - */ - caption: PropTypes.node, - - /** - * Provide a function that is called when menu is closed - */ - onCloseButtonClick: PropTypes.func, - - /** - * Provide a description for "close" icon that can be read by screen readers - */ - iconDescription: PropTypes.string.isRequired, - - /** - * By default, this value is "toast". You can also provide an alternate type - * if it makes sense for the underlying `` and `` - */ - notificationType: PropTypes.string, - - /** - * Specify the close button should be disabled, or not - */ - hideCloseButton: PropTypes.bool, - - /** - * Specify an optional duration the notification should be closed in - */ - timeout: PropTypes.number, - }; - - static defaultProps = { - kind: 'error', - title: 'provide a title', - caption: 'provide a caption', - role: 'alert', - notificationType: 'toast', - iconDescription: 'closes notification', - onCloseButtonClick: () => {}, - hideCloseButton: false, - timeout: 0, - }; - - state = { - open: true, - }; - - componentDidMount() { - if (this.props.timeout) { - setTimeout(() => { - this.handleCloseButtonClick(); - }, this.props.timeout); +export function ToastNotification({ + role, + notificationType, + onCloseButtonClick, // eslint-disable-line + iconDescription, // eslint-disable-line + className, + caption, + subtitle, + title, + kind, + lowContrast, + hideCloseButton, + children, + timeout, + ...rest +}) { + const [isOpen, setIsOpen] = useState(true); + const containerClassName = cx(className, { + [`${prefix}--toast-notification`]: true, + [`${prefix}--toast-notification--low-contrast`]: lowContrast, + [`${prefix}--toast-notification--${kind}`]: kind, + }); + + function handleCloseButtonClick(event) { + setIsOpen(false); + onCloseButtonClick(event); + } + + useEffect(() => { + if (!timeout) { + return; } + + const timeoutId = window.setTimeout(() => { + setIsOpen(false); + onCloseButtonClick(event); + }, timeout); + + return () => { + window.clearTimeout(timeoutId); + }; + }, [onCloseButtonClick, timeout]); + + if (!isOpen) { + return null; } - handleCloseButtonClick = evt => { - this.setState({ open: false }); - this.props.onCloseButtonClick(evt); - }; + return ( +
+ + + {children} + + {!hideCloseButton && ( + + )} +
+ ); +} - render() { - if (!this.state.open) { - return null; - } +ToastNotification.propTypes = { + /** + * Pass in the children that will be rendered within the ToastNotification + */ + children: PropTypes.node, + + /** + * Specify an optional className to be applied to the notification box + */ + className: PropTypes.string, + + /** + * Specify what state the notification represents + */ + kind: PropTypes.oneOf(['error', 'info', 'success', 'warning']).isRequired, + + /** + * Specify whether you are using the low contrast variant of the ToastNotification. + */ + lowContrast: PropTypes.bool, + + /** + * Specify the title + */ + title: PropTypes.string.isRequired, + + /** + * Specify the sub-title + */ + subtitle: PropTypes.node, + + /** + * By default, this value is "alert". You can also provide an alternate + * role if it makes sense from the accessibility-side + */ + role: PropTypes.string.isRequired, + + /** + * Specify the caption + */ + caption: PropTypes.node, + + /** + * Provide a function that is called when menu is closed + */ + onCloseButtonClick: PropTypes.func, + + /** + * Provide a description for "close" icon that can be read by screen readers + */ + iconDescription: PropTypes.string.isRequired, + + /** + * By default, this value is "toast". You can also provide an alternate type + * if it makes sense for the underlying `` and `` + */ + notificationType: PropTypes.string, + + /** + * Specify the close button should be disabled, or not + */ + hideCloseButton: PropTypes.bool, + + /** + * Specify an optional duration the notification should be closed in + */ + timeout: PropTypes.number, +}; - const { - role, - notificationType, - onCloseButtonClick, // eslint-disable-line - iconDescription, // eslint-disable-line - className, - caption, - subtitle, - title, - kind, - lowContrast, - hideCloseButton, - ...other - } = this.props; - - const classes = classNames( - `${prefix}--toast-notification`, - { - [`${prefix}--toast-notification--low-contrast`]: lowContrast, - [`${prefix}--toast-notification--${kind}`]: kind, - }, - className - ); +ToastNotification.defaultProps = { + kind: 'error', + title: 'provide a title', + caption: 'provide a caption', + role: 'alert', + notificationType: 'toast', + iconDescription: 'closes notification', + onCloseButtonClick: () => {}, + hideCloseButton: false, + timeout: 0, +}; - return ( -
+export function InlineNotification({ + actions, + role, + notificationType, + onCloseButtonClick, // eslint-disable-line + iconDescription, // eslint-disable-line + className, + subtitle, + title, + kind, + lowContrast, + hideCloseButton, + children, + ...rest +}) { + const [isOpen, setIsOpen] = useState(true); + const containerClassName = cx(className, { + [`${prefix}--inline-notification`]: true, + [`${prefix}--inline-notification--low-contrast`]: lowContrast, + [`${prefix}--inline-notification--${kind}`]: kind, + [`${prefix}--inline-notification--hide-close-button`]: hideCloseButton, + }); + + function handleCloseButtonClick(event) { + setIsOpen(false); + onCloseButtonClick(event); + } + + if (!isOpen) { + return null; + } + + return ( +
+
- {this.props.children} + {children} - {!hideCloseButton && ( - - )}
- ); - } + {actions} + {!hideCloseButton && ( + + )} +
+ ); } -export class InlineNotification extends Component { - static propTypes = { - /** - * Pass in the action nodes that will be rendered within the InlineNotification - */ - actions: PropTypes.node, - - /** - * Pass in the children that will be rendered within the InlineNotification - */ - children: PropTypes.node, - - /** - * Specify an optional className to be applied to the notification box - */ - className: PropTypes.string, - - /** - * Specify what state the notification represents - */ - kind: PropTypes.oneOf(['error', 'info', 'success', 'warning']).isRequired, - - /** - * Specify whether you are using the low contrast variant of the InlineNotification. - */ - lowContrast: PropTypes.bool, - - /** - * Specify the title - */ - title: PropTypes.string.isRequired, - - /** - * Specify the sub-title - */ - subtitle: PropTypes.node, - - /** - * By default, this value is "alert". You can also provide an alternate - * role if it makes sense from the accessibility-side - */ - role: PropTypes.string.isRequired, - - /** - * Provide a function that is called when menu is closed - */ - onCloseButtonClick: PropTypes.func, - - /** - * Provide a description for "close" icon that can be read by screen readers - */ - iconDescription: PropTypes.string.isRequired, - - /** - * By default, this value is "inline". You can also provide an alternate type - * if it makes sense for the underlying `` and `` - */ - notificationType: PropTypes.string, - - /** - * Specify the close button should be disabled, or not - */ - hideCloseButton: PropTypes.bool, - }; - - static defaultProps = { - role: 'alert', - notificationType: 'inline', - iconDescription: 'closes notification', - onCloseButtonClick: () => {}, - hideCloseButton: false, - }; - - state = { - open: true, - }; - - handleCloseButtonClick = evt => { - this.setState({ open: false }); - this.props.onCloseButtonClick(evt); - }; - - render() { - if (!this.state.open) { - return null; - } - - const { - actions, - role, - notificationType, - onCloseButtonClick, // eslint-disable-line - iconDescription, // eslint-disable-line - className, - subtitle, - title, - kind, - lowContrast, - hideCloseButton, - ...other - } = this.props; - - const classes = classNames( - `${prefix}--inline-notification`, - { - [`${prefix}--inline-notification--low-contrast`]: lowContrast, - [`${prefix}--inline-notification--${kind}`]: kind, - [`${prefix}--inline-notification--hide-close-button`]: hideCloseButton, - }, - className - ); +InlineNotification.propTypes = { + /** + * Pass in the action nodes that will be rendered within the InlineNotification + */ + actions: PropTypes.node, + + /** + * Pass in the children that will be rendered within the InlineNotification + */ + children: PropTypes.node, + + /** + * Specify an optional className to be applied to the notification box + */ + className: PropTypes.string, + + /** + * Specify what state the notification represents + */ + kind: PropTypes.oneOf(['error', 'info', 'success', 'warning']).isRequired, + + /** + * Specify whether you are using the low contrast variant of the InlineNotification. + */ + lowContrast: PropTypes.bool, + + /** + * Specify the title + */ + title: PropTypes.string.isRequired, + + /** + * Specify the sub-title + */ + subtitle: PropTypes.node, + + /** + * By default, this value is "alert". You can also provide an alternate + * role if it makes sense from the accessibility-side + */ + role: PropTypes.string.isRequired, + + /** + * Provide a function that is called when menu is closed + */ + onCloseButtonClick: PropTypes.func, + + /** + * Provide a description for "close" icon that can be read by screen readers + */ + iconDescription: PropTypes.string.isRequired, + + /** + * By default, this value is "inline". You can also provide an alternate type + * if it makes sense for the underlying `` and `` + */ + notificationType: PropTypes.string, + + /** + * Specify the close button should be disabled, or not + */ + hideCloseButton: PropTypes.bool, +}; - return ( -
-
- - - {this.props.children} - -
- {actions} - {!hideCloseButton && ( - - )} -
- ); - } -} +InlineNotification.defaultProps = { + role: 'alert', + notificationType: 'inline', + iconDescription: 'closes notification', + onCloseButtonClick: () => {}, + hideCloseButton: false, +};