Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Hoist dialog #1497

Closed
wants to merge 54 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
471594f
First cut of hoist dialog with isDraggable option.
cnrudd Dec 26, 2019
7da244f
Removed unused imports.
cnrudd Dec 26, 2019
2bab03f
Added canMaskClickClose prop and maskClick handler
cnrudd Dec 27, 2019
8a90e0f
Fix maskclick handler
cnrudd Dec 27, 2019
ec01ac3
added todo
cnrudd Dec 27, 2019
73ccc65
Support autoFocus: true on dialog children
cnrudd Jan 2, 2020
107c490
Replaced react-draggable with react-rnd, which uses react-draggable.
cnrudd Jan 3, 2020
90f5f42
Added --xh-dialog-box-shadow
cnrudd Jan 3, 2020
d6650a3
Support for resizing
cnrudd Jan 3, 2020
9a7f05b
Fix repaint bug
cnrudd Jan 6, 2020
b3fe561
Remove redundant use of splitLayoutProps
cnrudd Jan 6, 2020
2a65a78
Remove bug comment and fix. Bug existed only on chrome 78.3x . Upgr…
cnrudd Jan 6, 2020
c8f401b
Refactored with elementfactory.
cnrudd Jan 6, 2020
8e347e3
Remove need for constructor.
cnrudd Jan 6, 2020
3af10ed
Converted Dialog from class comp to function comp.
cnrudd Jan 7, 2020
a3b640b
Support resizable config
cnrudd Jan 7, 2020
d583d76
Refactor, with dialogHeader impl
cnrudd Jan 7, 2020
fad1660
Linting.
cnrudd Jan 8, 2020
88b0d45
Let dialogs autocenter based on content dimensions;
cnrudd Jan 8, 2020
c06514e
Use given width and height props if resizeable
cnrudd Jan 14, 2020
42a4d00
throw if no dims props with resizable = true
cnrudd Jan 14, 2020
d7d7a10
Add Maximize Minimize toggle if resizable.
cnrudd Jan 14, 2020
41c129c
Implemented localstorage statemodel on dialog.
cnrudd Jan 17, 2020
cfaa2af
Improve handling of 0 position values.
cnrudd Jan 17, 2020
e3169c1
Improve safe state loading on dialog.
cnrudd Jan 18, 2020
1d57410
Remove unnecessary setting of defaults on RnD, fix unMaximize.
cnrudd Jan 18, 2020
21f9f5d
Support x, y props.
cnrudd Jan 18, 2020
10f08c2
Merge branch 'develop' into hoistDialog
cnrudd Jan 21, 2020
40c27b5
Merge branch 'develop' into hoistDialog
cnrudd Jan 21, 2020
28c1175
Cleanup setState on render logic
cnrudd Jan 23, 2020
ffb7b14
Unifiied dialog sub component with mask and closeOnOutsideClick props.
cnrudd Feb 2, 2020
f320af6
added --xh-dialog-box-shadow
cnrudd Feb 9, 2020
34475a7
Support style passthrough and zIndex prop
cnrudd Feb 9, 2020
07fb4bb
Merge branch 'hoistDialog' of github.com:exhi/hoist-react into hoistD…
cnrudd Feb 9, 2020
4c38c76
--xh-dialog-box-shadow dark theme
cnrudd Feb 9, 2020
321de51
Improve takeup of style & zIndex & rndoptions
cnrudd Feb 10, 2020
2f934ec
Cleanup prop and model passing.
cnrudd Feb 15, 2020
cf7e6c4
Remove unused zIndex
cnrudd Feb 15, 2020
b2bdf02
Support static base zindex for all dialogs
cnrudd Feb 15, 2020
4753e87
Better method name: calcPos -> calcCenteredPos.
cnrudd Feb 16, 2020
cc5a41c
React-rnd semver tweak.
cnrudd Feb 16, 2020
633563b
Put scss var decl on one line.
cnrudd Feb 16, 2020
8ed6342
Merge branch 'develop' into hoistDialog
cnrudd Feb 16, 2020
ea94ab1
Latest deps in yarn.lock
cnrudd Feb 16, 2020
8b8283a
Model passing cleanup.
cnrudd Feb 16, 2020
372a8c7
Fix setPositionState
cnrudd Feb 16, 2020
65816e0
Move showCloseButton to prop
cnrudd Feb 17, 2020
7e6d049
Rework as controlled component;
cnrudd Feb 18, 2020
9ac3175
Fix closeOnEscape handling
cnrudd Feb 19, 2020
ede3e35
convert closeOnEscape to prop
cnrudd Feb 19, 2020
3fdf152
Fix handling of specified size and width when not resizable.
cnrudd Feb 21, 2020
5f5c8b8
Merge branch 'develop' into hoistDialog
cnrudd Feb 21, 2020
272a2b0
Use Hoist Dialog for messages.
cnrudd Feb 21, 2020
3d7d0c8
Match dialog styles to current hoist/bp dialog styles.
cnrudd Feb 21, 2020
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
19 changes: 12 additions & 7 deletions desktop/appcontainer/Message.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@
*/

import {form} from '@xh/hoist/cmp/form';
import {filler} from '@xh/hoist/cmp/layout';
import {filler, vframe} from '@xh/hoist/cmp/layout';
import {hoistCmp, uses} from '@xh/hoist/core';
import {MessageModel} from '@xh/hoist/appcontainer/MessageModel';
import {button} from '@xh/hoist/desktop/cmp/button';
import {formField} from '@xh/hoist/desktop/cmp/form';
import {textInput} from '@xh/hoist/desktop/cmp/input';
import {toolbar} from '@xh/hoist/desktop/cmp/toolbar';
import {dialog, dialogBody} from '@xh/hoist/kit/blueprint';
import {dialog} from '@xh/hoist/desktop/cmp/dialog';
import {withDefault} from '@xh/hoist/utils/js';

import './Message.scss';
Expand All @@ -33,17 +33,22 @@ export const message = hoistCmp.factory({

return dialog({
isOpen: true,
isCloseButtonShown: false,
showCloseButton: false,
title: model.title,
icon: model.icon,
mask: true,
items: [
dialogBody(
model.message,
inputCmp()
),
vframe({
margin: '20px',
items: [
model.message,
inputCmp()
]
}),
bbar()
],
onClose: () => {if (model.cancelProps) model.doCancel();},
width: 500,
...props
});
}
Expand Down
286 changes: 286 additions & 0 deletions desktop/cmp/dialog/Dialog.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,286 @@
/*
* This file belongs to Hoist, an application development toolkit
* developed by Extremely Heavy Industries (www.xh.io | [email protected])
*
* Copyright © 2019 Extremely Heavy Industries Inc.
*/

import {useEffect} from 'react';
import PT from 'prop-types';
import ReactDOM from 'react-dom';
import {castArray, isFunction, merge} from 'lodash';

import {rnd} from '@xh/hoist/kit/react-rnd';
import {hoistCmp, uses, useContextModel, ModelPublishMode} from '@xh/hoist/core';
import {useOnMount, useOnUnmount} from '@xh/hoist/utils/react';
import {div, fragment, vframe} from '@xh/hoist/cmp/layout';
import {throwIf} from '@xh/hoist/utils/js';

import {DialogModel} from './DialogModel';
import {dialogHeader} from './impl/DialogHeader';

import './DialogStyles.scss';


export const [Dialog, dialog] = hoistCmp.withFactory({
displayName: 'Dialog',
model: uses(DialogModel, {
fromContext: false,
publishMode: ModelPublishMode.LIMITED,
createDefault: true
}),
memo: false,
className: 'xh-dialog',

render({model, ...props}) {
const {isOpen} = props,
maybeSetFocus = () => {
// always delay focus manipulation to just before repaint to prevent scroll jumping
window.requestAnimationFrame(() => {
const {containerElement: container} = model,
{activeElement} = document;

// containerElement may be undefined between component mounting and Portal rendering
// activeElement may be undefined in some rare cases in IE
if (container == null || activeElement == null || !isOpen) return;

const isFocusOutsideModal = !container.contains(activeElement);
if (isFocusOutsideModal) {
/**
* @see {@link https://github.com/facebook/react/blob/9fe1031244903e442de179821f1d383a9f2a59f2/packages/react-dom/src/shared/DOMProperty.js#L294}
* @see {@link https://github.com/facebook/react/blob/master/packages/react-dom/src/client/ReactDOMHostConfig.js#L379}
* for why we do not search for autofocus on dom element: TLDR: it's not there!
*/
const wrapperElement = container.querySelector('[tabindex]');
if (wrapperElement != null) {
wrapperElement.focus();
}
}
});
};

useOnMount(() => {
/**
* @see {@link{https://reactjs.org/docs/portals.html#event-bubbling-through-portals}
* @see {@link{https://github.com/palantir/blueprint/blob/develop/packages/core/src/components/portal/portal.tsx}
*/
model.portalContainer = document.getElementById(model.dialogRootId);

model.containerElement = document.createElement('div');
model.portalContainer.appendChild(model.containerElement);
model.setHasMounted(true);
});

useOnUnmount(() => {
model.portalContainer.removeChild(model.containerElement);
});

useEffect(() => {
// these need to be called on 2nd render cycle
// cannot be put into useOnMount
// todo: explore how to ensure called only once.
// (may not be necessary to ensure only called once, not seeing any re-renders)
maybeSetFocus();

const {width, height, x, y} = props;
model.positionDialogOnRender({width, height, x, y});
});

const {hasMounted} = model;

if (!isOpen || !hasMounted) {
document.body.style.overflow = null;
return null;
}

// do we need to store prior overflow setting to be able to reset it when modal closes?
document.body.style.overflow = 'hidden';

return ReactDOM.createPortal(
rndDialog(props),
model.containerElement
);

}
});

Dialog.propTypes = {
/** True to render the dialog */
isOpen: PT.bool,

/** Callback invoked when user interaction triggers onClose call
* (closeOnOutsideClick overlay, close button, escape key)
*
* */
onClose: PT.func,

/** An icon placed at the left-side of the dialog's header. */
icon: PT.element,

/** Title text added to the dialog's header. */
title: PT.oneOfType([PT.string, PT.node]),

/** True to show close button in dialog's header */
showCloseButton: PT.bool,

/** True to show a shaded background mask behind dialog. */
mask: PT.bool,

/** True to close dialog on click outside of dialog. */
closeOnOutsideClick: PT.bool,

/** True to close dialog with escape key (defaults to true) */
closeOnEscape: PT.bool,

/** Width of dialog */
width: PT.number,

/** Height of dialog */
height: PT.number,

/** Left edge position of dialog */
x: PT.number,

/** Top edge position of dialog */
y: PT.number,

/** Escape hatch to pass any ReactRnD props to ReactRnD comp */
RnDOptions: PT.object,

/** CSS style object passed into ReactRnD */
style: PT.object
};

const rndDialog = hoistCmp.factory({
render(props) {
const model = useContextModel(DialogModel),
{resizable, draggable} = model,
{width, height, mask, closeOnOutsideClick, RnDOptions = {}, style, onClose, closeOnEscape} = props;

throwIf(
resizable && (!width || !height),
'Resizable dialogs must also have width and height props set.'
);

const onDragStop = (evt, data) => {
// ignore drags on close or maximize button in title bar
if (evt.target.closest('button')) return;

if (!model.isMaximizedState) {
model.setPositionState({x: data.x, y: data.y});
}
if (isFunction(RnDOptions.onDragStop)) RnDOptions.onDragStop(evt, data);
};

const onResizeStop = (
evt,
resizeDirection,
domEl,
resizableDelta,
position
) => {
if (!model.isMaximizedState) {
const {
offsetWidth: width,
offsetHeight: height
} = domEl;
model.setSizeState({width, height});
model.setPositionState(position);
}
if (isFunction(RnDOptions.onResizeStop)) {
RnDOptions.onResizeStop(
evt,
resizeDirection,
domEl,
resizableDelta,
position
);
}
};

const onKeyDown = (evt) => {
switch (evt.key) {
case 'Escape':
if (closeOnEscape !== false) {
model.handleEscapKey(onClose);
}
break;
}
};

if (style) RnDOptions.style = style;
let zIndex = DialogModel.DIALOG_ZINDEX_BASE;
if (RnDOptions.style?.zIndex) zIndex += RnDOptions.style.zIndex;
merge(RnDOptions, {style: {zIndex}});

return fragment(
mask ? maskComp({zIndex}) : null,
closeOnOutsideClick ? clickCaptureComp({zIndex, onClose}) : null,
rnd({
ref: c => model.rndRef = c,
...RnDOptions,
disableDragging: !draggable,
enableResizing: {
bottom: resizable,
bottomLeft: resizable,
bottomRight: resizable,
left: resizable,
right: resizable,
top: resizable,
topLeft: resizable,
topRight: resizable
},
bounds: 'body',
dragHandleClassName: 'xh-dialog__header',
onDragStop,
onResizeStop,
item: div({
onKeyDown,
tabIndex: 0,
ref: model.dialogWrapperDivRef,
className: props.className,
item: content(props)
})
})
);
}
});

const maskComp = hoistCmp.factory(
({zIndex}) => div({className: 'xh-dialog-root__mask', style: {zIndex}})
);

const clickCaptureComp = hoistCmp.factory({
render({zIndex, onClose}) {
const model = useContextModel(DialogModel);

return div({
className: 'xh-dialog-root__click-capture',
style: {zIndex},
ref: model.clickCaptureCompRef,
onClick: (evt) => model.handleOutsideClick(evt, onClose)
});
}
});

const content = hoistCmp.factory({
render(props) {
const dialogModel = useContextModel(DialogModel),
{width, height} = props,
dims = dialogModel.resizable ? {
width: '100%',
height: '100%'
} : {
width,
height
};

return vframe({
...dims,
items: [
dialogHeader({dialogModel, ...props}),
...castArray(props.children)
]
});
}
});
Loading