Skip to content
This repository has been archived by the owner on Mar 9, 2021. It is now read-only.

[WIP] feat: add dialog component #251

Draft
wants to merge 1 commit into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 10 additions & 10 deletions demo/Demo/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,21 +29,21 @@ class Demo extends Component {
<h2>Examples</h2>
{/* setting children to null in the key to avoid stringify choking on potential jsx children */}
{states.map(state => {
const { renderAfter, ...thinState } = state;
const {
DEMO_renderAfter,
DEMO_renderBefore,
...thinState
} = state;
const componentMarkup = this.renderState(thinState);
const afterMarkup =
renderAfter && jsxStringify(renderAfter, stringifyConfig);
DEMO_renderAfter &&
jsxStringify(DEMO_renderAfter, stringifyConfig);

return (
<div
key={JSON.stringify({
...thinState,
children:
typeof state.children === 'string' ? state.children : null
})}
>
<div key={componentMarkup}>
{DEMO_renderBefore}
<Component {...thinState} />
{renderAfter}
{DEMO_renderAfter}
<Highlight>
{`${componentMarkup}${
afterMarkup ? `\n${afterMarkup}` : ''
Expand Down
3 changes: 2 additions & 1 deletion demo/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ const componentsList = [
'Card',
'ExpandCollapsePanel',
'TextField',
'Link'
'Link',
'Dialog'
].sort();

class App extends Component {
Expand Down
112 changes: 112 additions & 0 deletions demo/patterns/components/Dialog/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import React, { useState } from 'react';
import {
Dialog,
DialogHeading,
DialogContent,
DialogFooter,
DialogActions,
Button
} from 'src/';
import DemoComponent from 'demo/Demo';

const Demo = () => {
const [modalShow, setModalShow] = useState(false);
const onModalClose = () => setModalShow(false);
const onModalLauncherClick = () => setModalShow(true);
const [alertShow, setAlertShow] = useState(false);
const onAlertClose = () => setAlertShow(false);
const onAlertLauncherClick = () => setAlertShow(true);

return (
<DemoComponent
component={Dialog}
states={[
{
DEMO_renderBefore: <h3>Modal Dialog</h3>,
children: (
<div>
<DialogContent>Hello worlds</DialogContent>
<DialogFooter>
<Button onClick={onModalClose}>ok</Button>
<Button variant="secondary" onClick={onModalClose}>
cancel
</Button>
</DialogFooter>
</div>
),
onClose: onModalClose,
show: modalShow,
heading: <DialogHeading>Hello</DialogHeading>,
DEMO_renderAfter: (
<Button onClick={onModalLauncherClick}>Launch Modal Dialog!</Button>
)
},
{
DEMO_renderBefore: <h3>Alert Dialog</h3>,
alert: true,
show: alertShow,
onClose: onAlertClose,
children: (
<div>
Do you accept the terms?
<DialogActions>
<Button onClick={onAlertClose}>Accept</Button>
<Button onClick={onAlertClose} variant="secondary">
Decline
</Button>
</DialogActions>
</div>
),
DEMO_renderAfter: (
<Button variant="secondary" onClick={onAlertLauncherClick}>
Launch Alert Dialog!
</Button>
)
}
]}
propDocs={{}}
/>
);
};

export default Demo;

/*
<div>
<Dialog
show={modalShow}
onClose={onModalClose}
heading={<DialogHeading>Testing123</DialogHeading>}
>
<DialogContent>
<p>Hello worlds</p>
</DialogContent>
<DialogFooter>
<Button onClick={onModalClose}>ok</Button>
<Button variant="secondary" onClick={onModalClose}>
cancel
</Button>
</DialogFooter>
</Dialog>
<Button onClick={onModalLauncherClick}>Launch Modal Dialog!</Button>
</div>
<div>
<Dialog
alert
show={alertShow}
onClose={onAlertClose}
heading={<DialogHeading>Testing123</DialogHeading>}
>
Do you accept the terms?
<DialogActions>
<Button onClick={onAlertClose}>Accept</Button>
<Button onClick={onAlertClose} variant="secondary">
Decline
</Button>
</DialogActions>
</Dialog>
<Button variant="secondary" onClick={onAlertLauncherClick}>
Launch Alert Dialog!
</Button>
</div>
*/
6 changes: 3 additions & 3 deletions demo/patterns/components/Toast/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export default class Demo extends Component {
show: type === 'confirmation',
autoHide: 5000,
onDismiss: () => this.onToastDismiss('confirmation'),
renderAfter: (
DEMO_renderAfter: (
<Button
onClick={() => this.onTriggerClick('confirmation')}
buttonRef={el => (this.confirmation = el)}
Expand All @@ -58,7 +58,7 @@ export default class Demo extends Component {
children: 'The toast is getting toasty...',
onDismiss: () => this.onToastDismiss('caution'),
show: type === 'caution',
renderAfter: (
DEMO_renderAfter: (
<Button
variant="secondary"
onClick={() => this.onTriggerClick('caution')}
Expand All @@ -73,7 +73,7 @@ export default class Demo extends Component {
children:
'You burnt the toast! Check yourself before you wreck yourself...',
show: false,
renderAfter: (
DEMO_renderAfter: (
<Button
variant="error"
onClick={() => this.onTriggerClick('action-needed')}
Expand Down
105 changes: 105 additions & 0 deletions src/components/Dialog/Dialog.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import React, { useEffect, useState, createRef } from 'react';
import FocusTrap from 'focus-trap-react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import Offscreen from '../Offscreen';
import Scrim from '../Scrim';
import AriaIsolate from '../../utils/aria-isolate';

const Dialog = ({
show,
onClose,
forceAction,
className,
closeButtonText,
children,
heading,
alert,
...other
}) => {
const dialog = createRef();
const [deactivate, setDeactivate] = useState();
useEffect(
() => {
if (show) {
const isolator = new AriaIsolate(dialog.current);
setDeactivate(() => isolator.deactivate.bind(isolator));
isolator.activate();
return;
}

if (!deactivate) {
return;
}

deactivate();
},
[show]
);

return show ? (
<FocusTrap
focusTrapOptions={{
clickOutsideDeactivates: true,
onDeactivate: onClose,
escapeDeactivates: !forceAction,
initialFocus: alert
? '.dqpl-dialog-inner'
: '.dqpl-dialog-show .dqpl-modal-heading',
allowOutsideClick: () => false
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have ClickOutsideListener used elsewhere. It would be nice to converge on a single thing. I think we should evaluate the other places we use ClickOutsideListener to see if they need focus traps and adjust accordingly, maybe as a tech debt thing.

}}
>
<div
role={alert ? 'alertdialog' : 'dialog'}
className={classNames(className, {
'dqpl-modal': !alert,
'dqpl-alert': alert,
'dqpl-dialog-show': show
})}
ref={dialog}
{...other}
>
<div className="dqpl-dialog-inner" tabIndex={-1}>
{alert ? (
<div className="dqpl-content">{children}</div>
) : (
<>
<div className="dqpl-modal-header">
{heading}
{!forceAction && (
<button
className="dqpl-close dqpl-icon"
type="button"
onClick={onClose}
>
<div className="fa fa-close" aria-hidden="true" />
<Offscreen>{closeButtonText}</Offscreen>
</button>
)}
</div>
{children}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just being nitpicky here, but do you think this should be wrapped with a class? Alert has dqpl-content so wouldn't we want to possibly style all dialog content as well? I see there's <DialogContent> which I guess lets you opt in, but it feels reductive.

</>
)}
</div>
</div>
<Scrim show={show} />
</FocusTrap>
) : null;
};

Dialog.displayName = 'Dialog';
Dialog.defaultProps = {
closeButtonText: 'Close'
};
Dialog.propTypes = {
children: PropTypes.node.isRequired,
onClose: PropTypes.func.isRequired,
show: PropTypes.bool,
forceAction: PropTypes.bool,
className: PropTypes.string,
closeButtonText: PropTypes.string,
heading: PropTypes.node,
alert: PropTypes.bool
};

export default Dialog;
12 changes: 12 additions & 0 deletions src/components/Dialog/DialogActions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';

const DialogActions = ({ className, ...other }) => (
<div className={classNames('dqpl-buttons', className)} {...other} />
);
DialogActions.displayName = 'DialogActions';
DialogActions.propTypes = {
className: PropTypes.string
};
export default DialogActions;
12 changes: 12 additions & 0 deletions src/components/Dialog/DialogContent.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';

const DialogContent = ({ className, ...other }) => (
<div className={classNames('dqpl-content', className)} {...other} />
);
DialogContent.displayName = 'DialogContent';
DialogContent.propTypes = {
className: PropTypes.string
};
export default DialogContent;
12 changes: 12 additions & 0 deletions src/components/Dialog/DialogFooter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';

const DialogFooter = ({ className, ...other }) => (
<div className={classNames('dqpl-modal-footer', className)} {...other} />
);
DialogFooter.displayName = 'DialogFooter';
DialogFooter.propTypes = {
className: PropTypes.string
};
export default DialogFooter;
25 changes: 25 additions & 0 deletions src/components/Dialog/DialogHeading.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';

const DialogHeading = ({ level, className, ...other }) => {
const Heading = `h${level}`;
return (
<Heading
tabIndex={-1}
className={classNames('dqpl-modal-heading', className)}
{...other}
/>
);
};

DialogHeading.displayName = 'DialogHeading';
DialogHeading.defaultProps = {
level: 2
};
DialogHeading.propTypes = {
level: PropTypes.number,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider using PropTypes.oneOf([1,2,3,4,5,6]), since level={500} is invalid.

className: PropTypes.string
};

export default DialogHeading;
5 changes: 5 additions & 0 deletions src/components/Dialog/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export default from './Dialog';
export { default as DialogHeading } from './DialogHeading';
export { default as DialogContent } from './DialogContent';
export { default as DialogFooter } from './DialogFooter';
export { default as DialogActions } from './DialogActions';
7 changes: 7 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,13 @@ export {
default as ExpandCollapsePanel,
PanelTrigger
} from './components/ExpandCollapsePanel';
export {
default as Dialog,
DialogHeading,
DialogFooter,
DialogContent,
DialogActions
} from './components/Dialog';
/**
* Helpers / Utils
*/
Expand Down