diff --git a/packages/contact-center/cc-components/src/components/task/CampaignErrorDialog/CampaignErrorDialog.tsx b/packages/contact-center/cc-components/src/components/task/CampaignErrorDialog/CampaignErrorDialog.tsx new file mode 100644 index 000000000..45e7e7012 --- /dev/null +++ b/packages/contact-center/cc-components/src/components/task/CampaignErrorDialog/CampaignErrorDialog.tsx @@ -0,0 +1,64 @@ +import React, {useEffect, useRef} from 'react'; +import {Button, Text} from '@momentum-design/components/dist/react'; +import {CampaignErrorDialogProps, ERROR_TITLES, ERROR_MESSAGE} from './campaign-error-dialog.types'; +import {withMetrics} from '@webex/cc-ui-logging'; +import './campaign-error-dialog.style.scss'; + +const CampaignErrorDialog: React.FunctionComponent = ({errorType, isOpen, onClose}) => { + const dialogRef = useRef(null); + + useEffect(() => { + const dialog = dialogRef.current; + if (!dialog) return; + + if (isOpen && !dialog.open) { + dialog.showModal(); + } else if (!isOpen && dialog.open) { + dialog.close(); + } + }, [isOpen]); + + const handleKeyDown = (event: React.KeyboardEvent) => { + if (event.key === 'Escape') { + onClose(); + } + }; + + const handleClose = () => { + onClose(); + }; + + return ( + + + {ERROR_TITLES[errorType]} + + + {ERROR_MESSAGE} + +
+ +
+
+ ); +}; + +const CampaignErrorDialogWithMetrics = withMetrics(CampaignErrorDialog, 'CampaignErrorDialog'); +export default CampaignErrorDialogWithMetrics; diff --git a/packages/contact-center/cc-components/src/components/task/CampaignErrorDialog/campaign-error-dialog.style.scss b/packages/contact-center/cc-components/src/components/task/CampaignErrorDialog/campaign-error-dialog.style.scss new file mode 100644 index 000000000..3506d5752 --- /dev/null +++ b/packages/contact-center/cc-components/src/components/task/CampaignErrorDialog/campaign-error-dialog.style.scss @@ -0,0 +1,37 @@ +.campaign-error-dialog { + width: 25rem; + border-radius: 0.5rem; + border-color: transparent; + padding: 1rem; + box-shadow: 0rem 0.25rem 0.5rem 0rem rgba(0, 0, 0, 0.16), 0rem 0rem 0.0625rem 0rem rgba(0, 0, 0, 0.16); + + &::backdrop { + background-color: rgba(0, 0, 0, 0.5); + } + + .campaign-error-dialog-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 1rem; + } + + .campaign-error-dialog-title { + margin: 0; + flex: 1; + } + + .campaign-error-dialog-close { + margin-left: 0.5rem; + } + + .campaign-error-dialog-message { + margin-bottom: 1.5rem; + } + + .campaign-error-dialog-actions { + display: flex; + justify-content: flex-end; + gap: 0.5rem; + } +} diff --git a/packages/contact-center/cc-components/src/components/task/CampaignErrorDialog/campaign-error-dialog.types.ts b/packages/contact-center/cc-components/src/components/task/CampaignErrorDialog/campaign-error-dialog.types.ts new file mode 100644 index 000000000..78f35bbbd --- /dev/null +++ b/packages/contact-center/cc-components/src/components/task/CampaignErrorDialog/campaign-error-dialog.types.ts @@ -0,0 +1,16 @@ +export type CampaignErrorType = 'ACCEPT_FAILED' | 'SKIP_FAILED' | 'REMOVE_FAILED'; + +export interface CampaignErrorDialogProps { + errorType: CampaignErrorType; + isOpen: boolean; + onClose: () => void; +} + +export const ERROR_TITLES: Record = { + ACCEPT_FAILED: "Can't accept contact", + SKIP_FAILED: "Can't skip contact", + REMOVE_FAILED: "Can't remove contact", +}; + +export const ERROR_MESSAGE = + 'We ran into an issue connecting you with this contact. Check your network connection and try again.'; diff --git a/packages/contact-center/cc-components/src/index.ts b/packages/contact-center/cc-components/src/index.ts index d4d692fdb..64096a129 100644 --- a/packages/contact-center/cc-components/src/index.ts +++ b/packages/contact-center/cc-components/src/index.ts @@ -5,6 +5,7 @@ import CallControlCADComponent from './components/task/CallControlCAD/call-contr import IncomingTaskComponent from './components/task/IncomingTask/incoming-task'; import TaskListComponent from './components/task/TaskList/task-list'; import OutdialCallComponent from './components/task/OutdialCall/outdial-call'; +import CampaignErrorDialogComponent from './components/task/CampaignErrorDialog/CampaignErrorDialog'; export { UserStateComponent, @@ -14,8 +15,10 @@ export { IncomingTaskComponent, TaskListComponent, OutdialCallComponent, + CampaignErrorDialogComponent, }; export * from './components/StationLogin/constants'; export * from './components/StationLogin/station-login.types'; export * from './components/UserState/user-state.types'; export * from './components/task/task.types'; +export * from './components/task/CampaignErrorDialog/campaign-error-dialog.types'; diff --git a/packages/contact-center/cc-components/tests/components/task/CampaignErrorDialog/CampaignErrorDialog.tsx b/packages/contact-center/cc-components/tests/components/task/CampaignErrorDialog/CampaignErrorDialog.tsx new file mode 100644 index 000000000..f8920e10f --- /dev/null +++ b/packages/contact-center/cc-components/tests/components/task/CampaignErrorDialog/CampaignErrorDialog.tsx @@ -0,0 +1,166 @@ +import React from 'react'; +import {render, fireEvent, screen} from '@testing-library/react'; +import '@testing-library/jest-dom'; +import CampaignErrorDialogComponent from '../../../../src/components/task/CampaignErrorDialog/CampaignErrorDialog'; +import { + CampaignErrorDialogProps, + CampaignErrorType, + ERROR_TITLES, + ERROR_MESSAGE, +} from '../../../../src/components/task/CampaignErrorDialog/campaign-error-dialog.types'; + +// Mock HTMLDialogElement methods +HTMLDialogElement.prototype.showModal = jest.fn(); +HTMLDialogElement.prototype.close = jest.fn(); + +describe('CampaignErrorDialogComponent', () => { + const mockOnClose = jest.fn(); + + const defaultProps: CampaignErrorDialogProps = { + errorType: 'ACCEPT_FAILED', + isOpen: false, + onClose: mockOnClose, + }; + + beforeEach(() => { + jest.clearAllMocks(); + (HTMLDialogElement.prototype.showModal as jest.Mock).mockClear(); + (HTMLDialogElement.prototype.close as jest.Mock).mockClear(); + }); + + describe('Rendering', () => { + it('should render the dialog element', () => { + render(); + + const dialog = screen.getByTestId('campaign-error-dialog'); + expect(dialog).toBeInTheDocument(); + expect(dialog).toHaveClass('campaign-error-dialog'); + }); + + it('should render the correct title for ACCEPT_FAILED error type', () => { + render(); + + const title = screen.getByTestId('campaign-error-dialog-title'); + expect(title).toHaveTextContent(ERROR_TITLES.ACCEPT_FAILED); + expect(title).toHaveTextContent("Can't accept contact"); + }); + + it('should render the correct title for SKIP_FAILED error type', () => { + render(); + + const title = screen.getByTestId('campaign-error-dialog-title'); + expect(title).toHaveTextContent(ERROR_TITLES.SKIP_FAILED); + expect(title).toHaveTextContent("Can't skip contact"); + }); + + it('should render the correct title for REMOVE_FAILED error type', () => { + render(); + + const title = screen.getByTestId('campaign-error-dialog-title'); + expect(title).toHaveTextContent(ERROR_TITLES.REMOVE_FAILED); + expect(title).toHaveTextContent("Can't remove contact"); + }); + + it('should render the error message', () => { + render(); + + const message = screen.getByTestId('campaign-error-dialog-message'); + expect(message).toHaveTextContent(ERROR_MESSAGE); + expect(message).toHaveTextContent( + 'We ran into an issue connecting you with this contact. Check your network connection and try again.' + ); + }); + + it('should render the OK button', () => { + render(); + + const okButton = screen.getByTestId('campaign-error-dialog-ok-button'); + expect(okButton).toBeInTheDocument(); + expect(okButton).toHaveTextContent('OK'); + }); + }); + + describe('Dialog Open/Close Behavior', () => { + it('should call showModal when isOpen changes to true', () => { + const {rerender} = render(); + + expect(HTMLDialogElement.prototype.showModal).not.toHaveBeenCalled(); + + rerender(); + + expect(HTMLDialogElement.prototype.showModal).toHaveBeenCalledTimes(1); + }); + + it('should call close when isOpen changes to false', () => { + const {rerender} = render(); + + // Simulate dialog being open + const dialog = screen.getByTestId('campaign-error-dialog') as HTMLDialogElement; + Object.defineProperty(dialog, 'open', {value: true, writable: true}); + + rerender(); + + expect(HTMLDialogElement.prototype.close).toHaveBeenCalledTimes(1); + }); + }); + + describe('User Interactions', () => { + it('should call onClose when OK button is clicked', () => { + render(); + + const okButton = screen.getByTestId('campaign-error-dialog-ok-button'); + fireEvent.click(okButton); + + expect(mockOnClose).toHaveBeenCalledTimes(1); + }); + + it('should call onClose when Escape key is pressed', () => { + render(); + + const dialog = screen.getByTestId('campaign-error-dialog'); + fireEvent.keyDown(dialog, {key: 'Escape'}); + + expect(mockOnClose).toHaveBeenCalledTimes(1); + }); + + it('should not call onClose when other keys are pressed', () => { + render(); + + const dialog = screen.getByTestId('campaign-error-dialog'); + fireEvent.keyDown(dialog, {key: 'Enter'}); + fireEvent.keyDown(dialog, {key: 'Tab'}); + fireEvent.keyDown(dialog, {key: 'Space'}); + + expect(mockOnClose).not.toHaveBeenCalled(); + }); + }); + + describe('Error Type Mapping', () => { + const errorTypes: CampaignErrorType[] = ['ACCEPT_FAILED', 'SKIP_FAILED', 'REMOVE_FAILED']; + + errorTypes.forEach((errorType) => { + it(`should display correct title for ${errorType}`, () => { + render(); + + const title = screen.getByTestId('campaign-error-dialog-title'); + expect(title).toHaveTextContent(ERROR_TITLES[errorType]); + }); + }); + }); + + describe('Accessibility', () => { + it('should have proper dialog structure', () => { + render(); + + const dialog = screen.getByTestId('campaign-error-dialog'); + expect(dialog.tagName).toBe('DIALOG'); + }); + + it('should have heading element for title', () => { + render(); + + const title = screen.getByTestId('campaign-error-dialog-title'); + expect(title).toBeInTheDocument(); + }); + }); +});