-
Notifications
You must be signed in to change notification settings - Fork 3
<Dialog /> - Basic Implementation #494
base: master
Are you sure you want to change the base?
Changes from all commits
dacb12a
47859ae
db3372a
9fc35d6
dfffbec
c199fea
783a94b
5bd6e6e
5233001
4d3ee66
03356eb
b7bd0f0
de20aae
f5b2792
19ce316
6531d7d
1b95901
1c60e69
99956fd
8cd1731
7477400
608c16a
1db174c
3558c03
d719244
067c71a
b76efc3
d420dab
580d11e
6fd4ab9
d5e2b93
3ed4ff4
db43eeb
e95a2f2
49310c2
3430b8b
8867f2a
f20de1d
18b7472
b977f84
3e5d14f
a7bdacb
047e8af
43fccbe
ac71636
d9853fa
2f70172
1234891
53b3ba9
5654b24
cc820ee
ad8a494
30fa7e2
a12ebbf
c693d9e
8391f23
6e81ce1
cfcf95b
5167996
035675f
d5241eb
afffdfa
73eb744
3b21f18
69d45f3
03ce22d
7f25143
f878c28
e85872d
da3ee0b
3af4d52
dd30bc9
fe7c675
4898d82
7a3d647
490612b
4a7a975
cebd260
b731f9e
77b9e47
e5c007d
86397fe
fc0f904
6fc6280
a92debd
76702f0
a510025
829d43c
5a9b70c
bb15af2
1a1e5a8
126e7e2
e441753
35235e0
bd6efcc
11b224c
5f66e3d
93ea389
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| .content { | ||
| width: 300px; | ||
| height: 300px; | ||
| font-size: 250px; | ||
| text-align: center; | ||
| vertical-align: middle; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 ( | ||
| <div> | ||
| <button data-automation-id="DIALOG_BUTTON" onClick={this.toggleOpen}>Open The Dialog!</button> | ||
| <Dialog className="root" isOpen={this.state.isOpen} onCancel={this.toggleOpen} onOk={this.toggleOpen}> | ||
| <div role="children" className="content">🌌</div> | ||
| </Dialog> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| private toggleOpen = () => { | ||
| this.setState({isOpen: !this.state.isOpen}); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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%; | ||
| } | ||
|
|
||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<HTMLDivElement> { | ||
| isOpen?: boolean; | ||
| onCancel?: (src: RequestCloseEvent) => void; | ||
| onOk?: () => void; | ||
| title?: string; | ||
| } | ||
|
|
||
| @stylable(styles) | ||
| @properties | ||
| export class Dialog extends React.PureComponent<DialogProps> { | ||
| public static defaultProps: DialogProps = { | ||
| isOpen: false, | ||
| onCancel: noop, | ||
| onOk: noop, | ||
| title: 'Dialog' | ||
| }; | ||
|
|
||
| public render() { | ||
| return ( | ||
| <Modal | ||
| className="root" | ||
| isOpen={!!this.props.isOpen} | ||
| onRequestClose={this.props.onCancel} | ||
| > | ||
| <div | ||
| data-automation-id="DIALOG_BODY" | ||
| onClick={this.onDialogBodyClick} | ||
| onKeyDown={this.handleKeyDown} | ||
| tabIndex={0} | ||
| > | ||
| <div className="header" role="header"> | ||
| <span data-automation-id="DIALOG_TITLE" role="title" className="title">{this.props.title}</span> | ||
| <Button | ||
| onClick={this.props.onCancel} | ||
| role="header-close-button" | ||
| data-automation-id="DIALOG_CLOSE" | ||
| > | ||
| X | ||
| </Button> | ||
| </div> | ||
| <div className="body" role="body"> | ||
| {this.props.children} | ||
| </div> | ||
| <div className="footer" role="footer"> | ||
| <Button | ||
| onClick={this.props.onCancel} | ||
| role="footer-close-button" | ||
| data-automation-id="DIALOG_CANCEL" | ||
| > | ||
| Cancel | ||
| </Button> | ||
| <Button | ||
| onClick={this.props.onOk} | ||
| role="footer-primary-button" | ||
| data-automation-id="DIALOG_PRIMARY" | ||
| > | ||
| OK | ||
| </Button> | ||
| </div> | ||
| </div> | ||
| </Modal> | ||
| ); | ||
| } | ||
|
|
||
| private handleKeyDown: React.KeyboardEventHandler<HTMLElement> = e => { | ||
| debugger; | ||
| switch (e.keyCode) { | ||
| case keycode('esc'): | ||
| const closeEvent: RequestCloseEvent = {source: 'escKeyPress'}; | ||
| this.props.onCancel!(closeEvent); | ||
| } | ||
| } | ||
|
|
||
| private onDialogBodyClick(e: React.SyntheticEvent<HTMLElement>) { | ||
| // without this, the click on the body propagates | ||
| // to the backdrop which closes the dialog | ||
| e.stopPropagation(); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export * from './dialog'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would expect this method to return |
||
| return this.getDialogElement('DIALOG_TITLE'); | ||
| } | ||
|
|
||
| public getButton(type: DialogButtonType) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. type |
||
| return this.getDialogElement(`DIALOG_${type}`); | ||
| } | ||
|
|
||
| private getDialogElement(elem: string): Element { | ||
| return this.select(this.root.getAttribute('data-automation-id')!, elem); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It seems to me like if you're fetching an element from the MODAL content you should rely on the modal driver to do it. |
||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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('<Dialog />', () => { | ||
| 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 () => { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what does |
||
| const {driver: dialogDemo, waitForDom} = | ||
| clientRenderer | ||
| .render(<DialogDemo />) | ||
| .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(); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think that the casting will not be required if the |
||
|
|
||
| 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(<Dialog isOpen title={testTitle} />) | ||
| .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(<Dialog isOpen {...handlerProp} />) | ||
| .withDriver(DialogTestDriver); | ||
|
|
||
| const getButton: Element = dialog.getButton(button.type as DialogButtonType); | ||
|
|
||
| (getButton as HTMLButtonElement).click(); | ||
|
|
||
| await waitFor(() => expect(onClick).to.have.been.calledOnce); | ||
| }); | ||
| }); | ||
|
|
||
| }); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would avoid a default here