diff --git a/src/components/notification/Notification.js b/src/components/notification/Notification.js.flow similarity index 100% rename from src/components/notification/Notification.js rename to src/components/notification/Notification.js.flow diff --git a/src/components/notification/Notification.tsx b/src/components/notification/Notification.tsx new file mode 100644 index 0000000000..adfe348200 --- /dev/null +++ b/src/components/notification/Notification.tsx @@ -0,0 +1,148 @@ +import * as React from 'react'; +import { defineMessages, injectIntl } from 'react-intl'; +import type { IntlShape } from 'react-intl'; +import classNames from 'classnames'; + +import { + AlertCircle, + InformationCircle, + CheckmarkCircle, + AlertTriangle, + XMark, +} from '@box/blueprint-web-assets/icons/Medium'; + +import InfoBadge16 from '../../icon/line/InfoBadge16'; +import CircleCheck16 from '../../icon/line/CircleCheck16'; +import TriangleAlert16 from '../../icon/line/TriangleAlert16'; + +import XBadge16 from '../../icon/line/XBadge16'; +import X16 from '../../icon/fill/X16'; + +import type { NotificationType } from '../../common/types/core'; + +import './Notification.scss'; + +// @NOTE: We can't import these constants from ./constant.js because `react-docgen` +// can't handle imported variables appear in propTypes +// see https://github.com/reactjs/react-docgen/issues/33 +const DURATION_SHORT = 'short'; +const DURATION_LONG = 'long'; +const OVERFLOW_WRAP = 'wrap'; +const TYPE_DEFAULT = 'default'; +const TYPE_INFO = 'info'; +const TYPE_WARN = 'warn'; +const TYPE_ERROR = 'error'; + +const DURATION_TIMES = { + [DURATION_SHORT]: 5000, + [DURATION_LONG]: 10000, +}; + +const ICON_RENDERER: Record React.ReactElement> = { + [TYPE_DEFAULT]: useV2Icons => (useV2Icons ? : ), + [TYPE_ERROR]: useV2Icons => (useV2Icons ? : ), + [TYPE_INFO]: useV2Icons => (useV2Icons ? : ), + [TYPE_WARN]: useV2Icons => (useV2Icons ? : ), +}; + +const messages = defineMessages({ + clearNotificationButtonText: { + defaultMessage: 'Clear Notification', + description: 'Button to clear notification', + id: 'boxui.notification.clearNotification', + }, +}); + +export interface NotificationProps { + /** + * The contents of the `Notification`. + * - Notification text must be wrapped in a `` tag. + * - Notification buttons must be the ` + + ); + } +} + +export default injectIntl(Notification); diff --git a/src/components/notification/NotificationsWrapper.js b/src/components/notification/NotificationsWrapper.js.flow similarity index 100% rename from src/components/notification/NotificationsWrapper.js rename to src/components/notification/NotificationsWrapper.js.flow diff --git a/src/components/notification/NotificationsWrapper.tsx b/src/components/notification/NotificationsWrapper.tsx new file mode 100644 index 0000000000..5bb372f674 --- /dev/null +++ b/src/components/notification/NotificationsWrapper.tsx @@ -0,0 +1,18 @@ +import * as React from 'react'; + +import FocusTrap from '../focus-trap'; +import Portal from '../portal'; + +export interface NotificationsWrapperProps { + /** Notification elements to render inside the live region */ + children?: React.ReactNode; +} + +const NotificationsWrapper = ({ children }: NotificationsWrapperProps) => ( + // @ts-ignore Portal forwards children and extra HTML attributes at runtime + + {children ? {children} : null} + +); + +export default NotificationsWrapper; diff --git a/src/components/notification/__tests__/Notification.test.js b/src/components/notification/__tests__/Notification.test.tsx similarity index 80% rename from src/components/notification/__tests__/Notification.test.js rename to src/components/notification/__tests__/Notification.test.tsx index fe6eb5192d..1d2cc5c36d 100644 --- a/src/components/notification/__tests__/Notification.test.js +++ b/src/components/notification/__tests__/Notification.test.tsx @@ -7,7 +7,7 @@ import { TYPE_DEFAULT, TYPE_INFO, TYPE_WARN, TYPE_ERROR } from '../constants'; import { Notification } from '..'; const sandbox = sinon.sandbox.create(); -let clock; +let clock: ReturnType; describe('components/notification/Notification', () => { beforeEach(() => { @@ -22,7 +22,7 @@ describe('components/notification/Notification', () => { test('should render a notification when initialized', () => { const wrapper = mount(test); - expect(wrapper.find('div.notification').length).toBe(1); + expect(wrapper.find('div.notification')).toHaveLength(1); expect(wrapper.find('span').text()).toEqual('test'); }); @@ -53,13 +53,13 @@ describe('components/notification/Notification', () => { const XBadge16Count = type === TYPE_ERROR ? 1 : 0; const TriangleAlert16Count = type === TYPE_WARN ? 1 : 0; - expect(component.find('InfoBadge16').length).toBe(infoBadge16Count); - expect(component.find('XBadge16').length).toBe(XBadge16Count); - expect(component.find('CircleCheck16').length).toBe(CircleCheck16Count); - expect(component.find('TriangleAlert16').length).toBe(TriangleAlert16Count); + expect(component.find('InfoBadge16')).toHaveLength(infoBadge16Count); + expect(component.find('XBadge16')).toHaveLength(XBadge16Count); + expect(component.find('CircleCheck16')).toHaveLength(CircleCheck16Count); + expect(component.find('TriangleAlert16')).toHaveLength(TriangleAlert16Count); // Does not render v2 icons - expect(component.find(`svg[role="img"]`).length).toBe(0); + expect(component.find(`svg[role="img"]`)).toHaveLength(0); }); test('should render v2 icons when useV2Icons is true', () => { @@ -70,30 +70,32 @@ describe('components/notification/Notification', () => { ); // Type icon and Close button - expect(component.find(`svg[role="img"]`).length).toBe(2); + expect(component.find(`svg[role="img"]`)).toHaveLength(2); // Does not render local icons - expect(component.find('InfoBadge16').length).toBe(0); - expect(component.find('XBadge16').length).toBe(0); - expect(component.find('CircleCheck16').length).toBe(0); - expect(component.find('TriangleAlert16').length).toBe(0); + expect(component.find('InfoBadge16')).toHaveLength(0); + expect(component.find('XBadge16')).toHaveLength(0); + expect(component.find('CircleCheck16')).toHaveLength(0); + expect(component.find('TriangleAlert16')).toHaveLength(0); }); }); - [ - { - overflowOption: undefined, - expectedClass: 'wrap', - }, - { - overflowOption: 'wrap', - expectedClass: 'wrap', - }, - { - overflowOption: 'ellipsis', - expectedClass: 'ellipsis', - }, - ].forEach(({ overflowOption, expectedClass }) => { + ( + [ + { + overflowOption: undefined, + expectedClass: 'wrap', + }, + { + overflowOption: 'wrap', + expectedClass: 'wrap', + }, + { + overflowOption: 'ellipsis', + expectedClass: 'ellipsis', + }, + ] as const + ).forEach(({ overflowOption, expectedClass }) => { test(`should render a notification with ${expectedClass} styling when passed the ${overflowOption} overflow option`, () => { const component = mount(test); diff --git a/src/components/notification/__tests__/NotificationsWrapper.test.js b/src/components/notification/__tests__/NotificationsWrapper.test.tsx similarity index 87% rename from src/components/notification/__tests__/NotificationsWrapper.test.js rename to src/components/notification/__tests__/NotificationsWrapper.test.tsx index e2a9b38612..392eeff17f 100644 --- a/src/components/notification/__tests__/NotificationsWrapper.test.js +++ b/src/components/notification/__tests__/NotificationsWrapper.test.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import { shallow } from 'enzyme'; import NotificationsWrapper from '../NotificationsWrapper'; import Notification from '../Notification'; @@ -8,7 +9,7 @@ describe('components/notification/NotificationsWrapper', () => { const wrapper = shallow(); expect(wrapper.is('Portal')).toBeTruthy(); expect(wrapper.hasClass('notifications-wrapper')).toBeTruthy(); - expect(wrapper.props('aria-live')).toBeTruthy(); + expect(wrapper.props()).toBeTruthy(); }); test('should render a focus trap', () => { @@ -18,7 +19,7 @@ describe('components/notification/NotificationsWrapper', () => { , ); const focusTrap = wrapper.find('FocusTrap'); - expect(focusTrap.length).toEqual(1); + expect(focusTrap).toHaveLength(1); }); test('should not render focusTrap if there are no children', () => { @@ -34,6 +35,6 @@ describe('components/notification/NotificationsWrapper', () => { , ); - expect(wrapper.find('Notification').length).toEqual(2); + expect(wrapper.find('Notification')).toHaveLength(2); }); }); diff --git a/src/components/notification/constants.js b/src/components/notification/constants.js.flow similarity index 100% rename from src/components/notification/constants.js rename to src/components/notification/constants.js.flow diff --git a/src/components/notification/constants.ts b/src/components/notification/constants.ts new file mode 100644 index 0000000000..3bc43e3791 --- /dev/null +++ b/src/components/notification/constants.ts @@ -0,0 +1,13 @@ +// Duration constants +export const DURATION_SHORT = 'short'; +export const DURATION_LONG = 'long'; + +// Type constants +export const TYPE_DEFAULT = 'default'; +export const TYPE_INFO = 'info'; +export const TYPE_WARN = 'warn'; +export const TYPE_ERROR = 'error'; + +// Overflow constants +export const OVERFLOW_WRAP = 'wrap'; +export const OVERFLOW_ELLIPSIS = 'ellipsis'; diff --git a/src/components/notification/index.js b/src/components/notification/index.js.flow similarity index 100% rename from src/components/notification/index.js rename to src/components/notification/index.js.flow diff --git a/src/components/notification/index.ts b/src/components/notification/index.ts new file mode 100644 index 0000000000..4275036251 --- /dev/null +++ b/src/components/notification/index.ts @@ -0,0 +1,7 @@ +import * as NotificationConstants from './constants'; +import Notification from './Notification'; +import NotificationsWrapper from './NotificationsWrapper'; + +export { Notification, NotificationConstants, NotificationsWrapper }; +export type { NotificationProps } from './Notification'; +export type { NotificationsWrapperProps } from './NotificationsWrapper'; diff --git a/src/components/notification/stories/Notification.stories.js b/src/components/notification/stories/Notification.stories.tsx similarity index 99% rename from src/components/notification/stories/Notification.stories.js rename to src/components/notification/stories/Notification.stories.tsx index 680b8e69b6..08dcafafc1 100644 --- a/src/components/notification/stories/Notification.stories.js +++ b/src/components/notification/stories/Notification.stories.tsx @@ -1,4 +1,3 @@ -// @flow import * as React from 'react'; import { IntlProvider } from 'react-intl'; diff --git a/src/components/notification/stories/NotificationsWrapper.stories.js b/src/components/notification/stories/NotificationsWrapper.stories.tsx similarity index 77% rename from src/components/notification/stories/NotificationsWrapper.stories.js rename to src/components/notification/stories/NotificationsWrapper.stories.tsx index f6d435f6c2..c429ab1cf2 100644 --- a/src/components/notification/stories/NotificationsWrapper.stories.js +++ b/src/components/notification/stories/NotificationsWrapper.stories.tsx @@ -1,4 +1,3 @@ -// @flow /* eslint-disable react-hooks/rules-of-hooks */ import * as React from 'react'; @@ -6,24 +5,31 @@ import Button from '../../button/Button'; import PrimaryButton from '../../primary-button/PrimaryButton'; import Notification from '../Notification'; +import { DURATION_SHORT, DURATION_LONG, TYPE_INFO, TYPE_WARN } from '../../../components/notification/constants'; import NotificationsWrapper from '../NotificationsWrapper'; import notes from './NotificationsWrapper.stories.md'; export const example = () => { const DATE = new Date('May 13, 2002 23:15:30').toTimeString(); - const [notificationData, setNotificationData] = React.useState({ + const [notificationData, setNotificationData] = React.useState<{ + id: number; + notifications: Map; + }>({ id: 0, notifications: new Map(), }); - const closeNotification = id => { + const closeNotification = (id: number) => { const notifications = new Map(notificationData.notifications); notifications.delete(id); setNotificationData({ ...notificationData, notifications }); }; - const addNotification = (duration, type) => { + const addNotification = ( + duration: typeof DURATION_SHORT | typeof DURATION_LONG, + type: typeof TYPE_INFO | typeof TYPE_WARN, + ) => { const { id } = notificationData; const { notifications } = notificationData; const notification = ( @@ -40,7 +46,7 @@ export const example = () => { return (
- {[...notificationData.notifications.values()]} + {Array.from(notificationData.notifications.values())} addNotification(undefined, 'warn')}> Display persistent notification