diff --git a/demo/components-demo.tsx b/demo/components-demo.tsx index e59a62048..32642f0ea 100644 --- a/demo/components-demo.tsx +++ b/demo/components-demo.tsx @@ -3,6 +3,7 @@ import * as React from 'react'; import {AutoCompleteDemo} from './components/auto-complete.demo'; import {CheckBoxDemo} from './components/checkbox-demo'; import {DatePickerDemo} from './components/date-picker-demo'; +import {DialogDemo} from './components/dialog-demo'; import {DropDownDemo} from './components/drop-down.demo'; import {ImageDemo} from './components/image-demo'; import {LoaderDemo} from './components/loader-demo'; @@ -397,6 +398,11 @@ export class ComponentsDemo extends React.Component {

Modal

+
+
+

Dialog

+ +
); } diff --git a/demo/components/dialog-demo.st.css b/demo/components/dialog-demo.st.css new file mode 100644 index 000000000..c83bfa87d --- /dev/null +++ b/demo/components/dialog-demo.st.css @@ -0,0 +1,7 @@ +.content { + width: 300px; + height: 300px; + font-size: 250px; + text-align: center; + vertical-align: middle; +} diff --git a/demo/components/dialog-demo.tsx b/demo/components/dialog-demo.tsx new file mode 100644 index 000000000..202ea7c39 --- /dev/null +++ b/demo/components/dialog-demo.tsx @@ -0,0 +1,30 @@ +import * as React from 'react'; +import {stylable} from 'wix-react-tools'; +import {Dialog} from '../../src'; +import styles from './dialog-demo.st.css'; + +export interface DialogDemoState { + isOpen: boolean; +} + +@stylable(styles) +export class DialogDemo extends React.Component<{}, DialogDemoState> { + public state: DialogDemoState = { + isOpen: false + }; + + public render() { + return ( +
+ + +
🌌
+
+
+ ); + } + + private toggleOpen = () => { + this.setState({isOpen: !this.state.isOpen}); + } +} diff --git a/src/components/dialog/dialog.st.css b/src/components/dialog/dialog.st.css new file mode 100644 index 000000000..80bf2dd5e --- /dev/null +++ b/src/components/dialog/dialog.st.css @@ -0,0 +1,38 @@ +@namespace "Dialog"; + +:import{ + -st-from: "../modal/modal.st.css"; + -st-default: Modal; +} + +.root { + -st-extends: Modal; + display: inline-block; +} + +.root::children { + border: 1px solid black; + border-radius: 5px; + background-color: lightgray; +} + +.header { + border-bottom: 1px solid black; +} + +.header .title { + margin-left: 5px; +} + +.header button { + float: right; + margin-right: 5px; +} + +.footer { + display: flex; + justify-content: space-around; + padding-bottom: 5%; +} + + diff --git a/src/components/dialog/dialog.tsx b/src/components/dialog/dialog.tsx new file mode 100644 index 000000000..e6b24ec87 --- /dev/null +++ b/src/components/dialog/dialog.tsx @@ -0,0 +1,87 @@ +import keycode = require('keycode'); +import * as React from 'react'; +import {properties, stylable} from 'wix-react-tools'; +import {noop} from '../../utils'; +import {Button} from '../button'; +import {Modal, RequestCloseEvent} from '../modal'; +import styles from './dialog.st.css'; + +export interface DialogProps extends React.HTMLAttributes { + isOpen?: boolean; + onCancel?: (src: RequestCloseEvent) => void; + onOk?: () => void; + title?: string; +} + +@stylable(styles) +@properties +export class Dialog extends React.PureComponent { + public static defaultProps: DialogProps = { + isOpen: false, + onCancel: noop, + onOk: noop, + title: 'Dialog' + }; + + public render() { + return ( + +
+
+ {this.props.title} + +
+
+ {this.props.children} +
+
+ + +
+
+
+ ); + } + + private handleKeyDown: React.KeyboardEventHandler = e => { + debugger; + switch (e.keyCode) { + case keycode('esc'): + const closeEvent: RequestCloseEvent = {source: 'escKeyPress'}; + this.props.onCancel!(closeEvent); + } + } + + private onDialogBodyClick(e: React.SyntheticEvent) { + // without this, the click on the body propagates + // to the backdrop which closes the dialog + e.stopPropagation(); + } +} diff --git a/src/components/dialog/index.ts b/src/components/dialog/index.ts new file mode 100644 index 000000000..20da8e550 --- /dev/null +++ b/src/components/dialog/index.ts @@ -0,0 +1 @@ +export * from './dialog'; diff --git a/src/components/index.ts b/src/components/index.ts index bdeeede7f..907c87bc6 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -3,6 +3,7 @@ export * from './button'; export * from './checkbox'; export * from './context-provider'; export * from './date-picker'; +export * from './dialog'; export * from './drop-down'; export * from './global-event'; export * from './image'; diff --git a/src/components/modal/modal.tsx b/src/components/modal/modal.tsx index faeca8c12..6ff60f01f 100644 --- a/src/components/modal/modal.tsx +++ b/src/components/modal/modal.tsx @@ -6,7 +6,7 @@ import {enableScrolling, stopScrolling} from '../../utils/stop-scrolling'; import {Portal} from '../portal'; import styles from './modal.st.css'; -export interface RequestCloseEvent extends React.SyntheticEvent { +export interface RequestCloseEvent { source: string; } @@ -48,7 +48,7 @@ export class Modal extends React.PureComponent { private onClick: React.EventHandler> = event => { const {target} = event; if (isElement(target)) { - const closeEvent: RequestCloseEvent = {...event, source: this.getDataFromNearestNode(target)}; + const closeEvent: RequestCloseEvent = {source: this.getDataFromNearestNode(target)}; this.props.onRequestClose!(closeEvent); } } diff --git a/test-kit/components/dialog-driver.ts b/test-kit/components/dialog-driver.ts new file mode 100644 index 000000000..ed856c311 --- /dev/null +++ b/test-kit/components/dialog-driver.ts @@ -0,0 +1,31 @@ +import {DriverBase} from 'test-drive-react'; +import {Dialog} from '../../src'; +import {ModalTestDriver} from './modal-driver'; + +export type DialogButtonType = 'CANCEL' | 'CLOSE' | 'PRIMARY'; + +export class DialogTestDriver extends DriverBase { + public static ComponentClass = Dialog; + public modalDriver: ModalTestDriver; + + constructor(getDialog: () => HTMLElement) { + super(getDialog); + this.modalDriver = new ModalTestDriver(getDialog); + } + + public get root(): Element { + return this.modalDriver.children[0]; + } + + public get title(): Element { + return this.getDialogElement('DIALOG_TITLE'); + } + + public getButton(type: DialogButtonType) { + return this.getDialogElement(`DIALOG_${type}`); + } + + private getDialogElement(elem: string): Element { + return this.select(this.root.getAttribute('data-automation-id')!, elem); + } +} diff --git a/test-kit/components/index.ts b/test-kit/components/index.ts index 33dd05211..97899230c 100644 --- a/test-kit/components/index.ts +++ b/test-kit/components/index.ts @@ -2,7 +2,9 @@ export * from './checkbox-driver'; export * from './tree-view-driver'; export * from './drop-down-driver'; export * from './image-driver'; +export * from './modal-driver'; export * from './portal-driver'; +export * from './dialog-driver'; export * from './modal-driver'; export * from './radio-group-driver'; export * from './selection-list-driver'; diff --git a/test/components/dialog.spec.tsx b/test/components/dialog.spec.tsx new file mode 100644 index 000000000..f59af5a6f --- /dev/null +++ b/test/components/dialog.spec.tsx @@ -0,0 +1,94 @@ +import * as React from 'react'; +import {ClientRenderer, DriverBase, expect, sinon, waitFor} from 'test-drive-react'; +import {DialogDemo} from '../../demo/components/dialog-demo'; +import {Dialog} from '../../src'; +import {DialogButtonType, DialogTestDriver} from '../../test-kit/components'; + +const dialogButtons = [ + {id: 'X', handler: 'onCancel', type: 'CLOSE'}, + {id: 'CANCEL', handler: 'onCancel', type: 'CANCEL'}, + {id: 'PRIMARY', handler: 'onOk', type: 'PRIMARY'} +]; + +class DialogDemoDriver extends DriverBase { + public static ComponentClass = DialogDemo; + public dialogDriver: DialogTestDriver; + + constructor(getDialogDemo: () => HTMLElement) { + super(getDialogDemo); + this.dialogDriver = new DialogTestDriver(getDialogDemo); + } + + public get dialog(): Element { + return this.dialogDriver.root; + } + + public get showDialogButton(): Element { + return this.select('DIALOG_BUTTON'); + } +} + +describe('', () => { + const clientRenderer = new ClientRenderer(); + + afterEach(() => clientRenderer.cleanup()); + + it('opens the dialog upon extra button click, and closes it upon clicking any of the buttons', async () => { + const {driver: dialogDemo, waitForDom} = + clientRenderer + .render() + .withDriver(DialogDemoDriver); + + const showDialogBtn = dialogDemo.showDialogButton as HTMLButtonElement; + showDialogBtn.click(); + + const dialogDriver = dialogDemo.dialogDriver; + + await waitForDom(() => expect(dialogDemo.dialog).to.be.present()); + + (dialogDriver.getButton('CLOSE') as HTMLButtonElement).click(); + + await waitForDom(() => expect(dialogDriver.modalDriver.root).to.be.absent()); + showDialogBtn.click(); + + (dialogDriver.getButton('CANCEL') as HTMLButtonElement).click(); + + await waitForDom(() => expect(dialogDriver.modalDriver.root).to.be.absent()); + showDialogBtn.click(); + + (dialogDriver.getButton('PRIMARY') as HTMLButtonElement).click(); + + await waitForDom(() => expect(dialogDriver.modalDriver.root).to.be.absent()); + }); + + it('displays the provided title', async () => { + const testTitle = 'Do you accept this?'; + const {driver: dialog, waitForDom} = clientRenderer + .render() + .withDriver(DialogTestDriver); + + await waitForDom(() => expect(dialog.title).to.have.text(testTitle)); + + }); + + dialogButtons.forEach(button => { + it(`invokes the callback handler provided for ${button.id} button`, async () => { + const onClick = sinon.spy(); + + const handlerProp = { + [button.handler]: onClick + }; + + const {driver: dialog} = clientRenderer + .render() + .withDriver(DialogTestDriver); + + const getButton: Element = dialog.getButton(button.type as DialogButtonType); + + (getButton as HTMLButtonElement).click(); + + await waitFor(() => expect(onClick).to.have.been.calledOnce); + }); + }); + +}); diff --git a/test/contracts/root-element.spec.tsx b/test/contracts/root-element.spec.tsx index 1b5a5d544..a80bc6fff 100644 --- a/test/contracts/root-element.spec.tsx +++ b/test/contracts/root-element.spec.tsx @@ -7,7 +7,7 @@ import {isReactComponent} from '../utils/is-react-component'; const allComponents = Object.keys(WixReactComponents); const failingComponents = [ - 'Portal', 'Popup', 'Modal', 'ContextProvider', 'GlobalEvent' + 'Portal', 'Popup', 'Modal', 'ContextProvider', 'GlobalEvent', 'Dialog' ]; describe('Root Element contract', () => {