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

Refactor Dialog and Popup to remove jquery #945

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 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
3 changes: 3 additions & 0 deletions packages/libs/wdk-client/src/Components/Overlays/Dialog.css
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ body.wdk-ModalOpen {
border-top-right-radius: 4px;
background: #eee;
padding: 0.6em 0.8em;
}

.wdk-DialogHeader.draggable {
cursor: move;
}

Expand Down
78 changes: 68 additions & 10 deletions packages/libs/wdk-client/src/Components/Overlays/Dialog.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import React, { ReactNode, useEffect, useRef } from 'react';
import React, {
CSSProperties,
ReactNode,
useEffect,
useRef,
useState,
} from 'react';
import Icon from '../../Components/Icon/Icon';
import { useBodyScrollManager } from '../../Components/Overlays/BodyScrollManager';
import Popup from '../../Components/Overlays/Popup';
Expand All @@ -20,10 +26,19 @@ type Props = {
onClose?: () => void;
};

const resizeStyling: CSSProperties = {
resize: 'both',
overflow: 'auto',
minHeight: 100,
};

function Dialog(props: Props) {
const headerNode = useRef<HTMLDivElement>(null);
useBodyScrollManager(props.open && !!props.modal);
useRestorePrevoiusFocus(props.open);
useRestorePreviousFocus(props.open);
const [isDragging, setIsDragging] = useState(false);
const [initialOffset, setInitialOffset] =
useState<{ left: number; top: number } | undefined>(undefined);

if (!props.open) return null;

Expand All @@ -37,14 +52,62 @@ function Dialog(props: Props) {
leftButtons,
} = props;

const handleDragStart = (e: React.MouseEvent) => {
setIsDragging(true);
if (headerNode.current && !initialOffset) {
const popupRect =
headerNode.current.parentElement?.getBoundingClientRect();
setInitialOffset({
left: e.clientX - (popupRect?.left ?? 0),
top: e.clientY - (popupRect?.top ?? 0),
});
}
};

const handleDrag = (e: React.MouseEvent) => {
if (isDragging && headerNode.current && initialOffset) {
const popupContainer = headerNode.current.parentElement;
if (popupContainer) {
const popupRect = popupContainer.getBoundingClientRect();
const left =
popupContainer.offsetLeft +
(e.clientX - popupRect.left) -
initialOffset.left;
const top =
popupContainer.offsetTop +
(e.clientY - popupRect.top) -
initialOffset.top;
popupContainer.style.left = left + 'px';
popupContainer.style.top = top + 'px';
}
}
};

const handleDragEnd = () => {
setIsDragging(false);
setInitialOffset(undefined);
};

const content = (
<div
onKeyDown={handleKeyDown}
className={makeClassName(props.className, '', props.modal && 'modal')}
style={props.resizable ? resizeStyling : undefined}
>
<div
ref={headerNode}
className={makeClassName(props.className, 'Header')}
className={makeClassName(
props.className,
'Header',
props.draggable ? ' draggable' : ''
)}
{...(props.draggable
? {
onMouseDown: handleDragStart,
onMouseMove: handleDrag,
onMouseUp: handleDragEnd,
}
: {})}
>
<div className={makeClassName(props.className, 'LeftButtons')}>
{leftButtons}
Expand All @@ -63,12 +126,7 @@ function Dialog(props: Props) {
);

return (
<Popup
className={makeClassName(props.className, 'PopupWrapper')}
dragHandleSelector={() => headerNode.current as HTMLDivElement}
open={props.open}
resizable={props.resizable}
>
<Popup className={makeClassName(props.className, 'PopupWrapper')}>
{content}
</Popup>
);
Expand All @@ -90,7 +148,7 @@ function makeClassName(className?: string, suffix = '', ...modifiers: any[]) {
);
}

function useRestorePrevoiusFocus(isOpen: boolean) {
function useRestorePreviousFocus(isOpen: boolean) {
const previousActiveRef = useRef<Element | null>();
useEffect(() => {
if (isOpen) {
Expand Down
144 changes: 7 additions & 137 deletions packages/libs/wdk-client/src/Components/Overlays/Popup.tsx
Original file line number Diff line number Diff line change
@@ -1,152 +1,22 @@
// Primitive component for creating a popup window

import $ from 'jquery';
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import TabbableContainer from '../../Components/Display/TabbableContainer';

type Props = {
/** Should the popup be visible or not? */
open: boolean;

resizable?: boolean;

className?: string;

/**
* Element which to append the draggable container. Defaults to
* `document.body`.
*/
parentSelector?: () => Element;

/**
* Element to use to constrain dragging. If set, the popup can only be
* dragged within the returned Element.
*/
containerSelector?: () => Element;

/** Should the popup be draggable? */
draggable?: boolean;

/**
* Set the element to use as a drag handle. This should be a descendent of the
* content root element. Only used if `draggable` is `true`.
*/
dragHandleSelector?: () => Element;

/** Content of popup */
children: React.ReactElement<any>;
};

// TODO Replace jQueryUI plugin with react-dnd
/**
* Popup window
*
* @example
* ```
* class App extends React.Component {
* render() {
* return (
* <div>
* <button type="button" onClick={() => this.setState({ open: true })>
* Open popup
* </button>
* <Popup
* open={this.state.open}
* draggable
* >
* <div>
* <h1>
* Some title
* <div className="buttons">
* <button type="button" onClick={() => this.setState({ open: false })}>
* <Icon fa="close"/>
* </button>
* </div>
* </h1>
* <div>Some content</div>
* </div>
* </Popup>
* </div>
* );
* }
* }
* ```
*/
class Popup extends React.Component<Props> {
static defaultProps = {
draggable: false,
};

containerNode?: HTMLElement;

popupNode: Element | null = null;

componentDidMount() {
// Create container node and attatch it to the parent node.
this.containerNode = document.createElement('div');
const parent =
this.props.parentSelector == null
? document.body
: this.props.parentSelector();
if (parent !== this.containerNode.parentNode) {
parent.appendChild(this.containerNode);
}

// Force this component to update, since the containerNode did not exist on
// the first render and we want to render the Portal now. This will also
// cause `componentDidUpdate` to be called.
this.forceUpdate();
}

componentDidUpdate() {
this._callJqueryWithProps();
}

componentWillUnmount() {
if (this.popupNode) $(this.popupNode).draggable('destroy');
if (this.containerNode) this.containerNode.remove();
}

_callJqueryWithProps() {
if (this.popupNode == null) return;
const $node = $(this.popupNode)
.draggable({
addClasses: false,
containment:
this.props.containerSelector == null
? 'document'
: this.props.containerSelector(),
handle:
this.props.dragHandleSelector == null
? false
: this.props.dragHandleSelector(),
})
.toggle(this.props.open);

if (this.props.resizable) {
$node.resizable({
handles: 'all',
minWidth: 100,
minHeight: 100,
});
}
}

render() {
const children = React.cloneElement(this.props.children, {
ref: (c: React.ReactInstance | null) =>
(this.popupNode = c && (ReactDOM.findDOMNode(c) as HTMLElement)),
});
const content = (
<TabbableContainer autoFocus className={this.props.className || ''}>
{children}
</TabbableContainer>
);
return this.containerNode
? ReactDOM.createPortal(content, this.containerNode)
: null;
}
function Popup({ className, children }: Props) {
const content = (
<TabbableContainer autoFocus className={className || ''}>
{children}
</TabbableContainer>
);
return ReactDOM.createPortal(content, document.body);
}

export default Popup;
Loading